DataCo Global - uma empresa global do ramo de supply chain¶

A Empresa DataCo Global é uma companhia de escala global que atua no setor de supply chain envolvendo diversos produtos desde eletrônicos, brinquedos, acessórios esportivos, acessórios para acampamento e até livros.

Aqui será realizada uma análise detalhada das suas vendas e lucros diante de diversos aspectos como categorias, departamentos e localização de clientes, avaliando também problemas logísticos como atrasos em entregas e problemas com operações fraudulentas.

Venha descobrir insights valiosos sobre a empresa, incluindo como andam as suas vendas, os seus lucros e suas operações de logística!

1. Importando as bibliotecas e o dataset

In [1]:
# Importando a biblioteca para manipulação de bases de dados
import pandas as pd
import locale

#Importando a biblioteca para manipulação algébrica
import numpy as np

# Bibliotecas para a EDA
import missingno
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
import seaborn as sns
from wordcloud import WordCloud

# Estatística
from scipy.stats import skew
from scipy.stats import chi2_contingency

# importando as funções Stratified K-Fold e train_test_split
from  sklearn.model_selection import train_test_split, StratifiedKFold

# Importando os modelos
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
from imblearn.ensemble import BalancedRandomForestClassifier

# Feature Importance
from sklearn.inspection import permutation_importance
from sklearn.feature_selection import RFE

# importando as funções para calcular a precisão, revocação, precision_recall_auc, roc_auc, medida F1 e acurácia
from sklearn.metrics import precision_score, recall_score, precision_recall_curve, auc, roc_auc_score, f1_score, accuracy_score, confusion_matrix

# Encoder para tratamento de variáveis categóricas
from category_encoders import CatBoostEncoder

# Pipelines
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline

# Importando biblioteca para tunagem de hiperparâmetros
import optuna as opt

# Setando diretório de trabalho
import os
os.chdir(r"C:\Users\edd-j\Downloads\Conteudos\Portfolio\Fraudes\Fraude_Supply_Chain")

# Configurar para não exibir warnings
from warnings import filterwarnings
filterwarnings('ignore')

# Exibindo todas as colunas das bases de dados
pd.set_option('display.max_columns', None)

# Ajustando a configuração de exibição do Pandas para mostrar todo o conteúdo das colunas
pd.set_option('display.max_colwidth', None)
In [2]:
# Importando bases de dados

df = pd.read_csv('DataCoSupplyChainDataset.csv', encoding='ISO-8859-1')
df_descricao = pd.read_csv('DescriptionDataCoSupplyChain.csv')

2. Dicionário dos dados

In [3]:
# Limpeza dos dados para remover caracteres extras como ':'
df_descricao['FIELDS'] = df_descricao['FIELDS'].str.strip()
df_descricao['DESCRIPTION'] = df_descricao['DESCRIPTION'].str.strip().str.lstrip(':').str.strip()

# Apresentando a descrição das colunas da base de dados
df_descricao
Out[3]:
FIELDS DESCRIPTION
0 Type Type of transaction made
1 Days for shipping (real) Actual shipping days of the purchased product
2 Days for shipment (scheduled) Days of scheduled delivery of the purchased product
3 Benefit per order Earnings per order placed
4 Sales per customer Total sales per customer made per customer
5 Delivery Status Delivery status of orders: Advance shipping , Late delivery , Shipping canceled , Shipping on time
6 Late_delivery_risk Categorical variable that indicates if sending is late (1), it is not late (0).
7 Category Id Product category code
8 Category Name Description of the product category
9 Customer City City where the customer made the purchase
10 Customer Country Country where the customer made the purchase
11 Customer Email Customer's email
12 Customer Fname Customer name
13 Customer Id Customer ID
14 Customer Lname Customer lastname
15 Customer Password Masked customer key
16 Customer Segment Types of Customers: Consumer , Corporate , Home Office
17 Customer State State to which the store where the purchase is registered belongs
18 Customer Street Street to which the store where the purchase is registered belongs
19 Customer Zipcode Customer Zipcode
20 Department Id Department code of store
21 Department Name Department name of store
22 Latitude Latitude corresponding to location of store
23 Longitude Longitude corresponding to location of store
24 Market Market to where the order is delivered : Africa , Europe , LATAM , Pacific Asia , USCA
25 Order City Destination city of the order
26 Order Country Destination country of the order
27 Order Customer Id Customer order code
28 order date (DateOrders) Date on which the order is made
29 Order Id Order code
30 Order Item Cardprod Id Product code generated through the RFID reader
31 Order Item Discount Order item discount value
32 Order Item Discount Rate Order item discount percentage
33 Order Item Id Order item code
34 Order Item Product Price Price of products without discount
35 Order Item Profit Ratio Order Item Profit Ratio
36 Order Item Quantity Number of products per order
37 Sales Value in sales
38 Order Item Total Total amount per order
39 Order Profit Per Order Order Profit Per Order
40 Order Region Region of the world where the order is delivered : Southeast Asia ,South Asia ,Oceania ,Eastern Asia, West Asia , West of USA , US Center , West Africa, Central Africa ,North Africa ,Western Europe ,Northern , Caribbean , South America ,East Africa ,Southern Europe , East of USA ,Canada ,Southern Africa , Central Asia , Europe , Central America, Eastern Europe , South of USA
41 Order State State of the region where the order is delivered
42 Order Status Order Status : COMPLETE , PENDING , CLOSED , PENDING_PAYMENT ,CANCELED , PROCESSING ,SUSPECTED_FRAUD ,ON_HOLD ,PAYMENT_REVIEW
43 Product Card Id Product code
44 Product Category Id Product category code
45 Product Description Product Description
46 Product Image Link of visit and purchase of the product
47 Product Name Product Name
48 Product Price Product Price
49 Product Status Status of the product stock :If it is 1 not available , 0 the product is available
50 Shipping date (DateOrders) Exact date and time of shipment
51 Shipping Mode The following shipping modes are presented : Standard Class , First Class , Second Class , Same Day

3. Visão Geral do Dataset

In [4]:
# Primeiras linhas do dataset
df.head()
Out[4]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode
0 DEBIT 3 4 91.250000 314.640015 Advance shipping 0 73 Sporting Goods Caguas Puerto Rico XXXXXXXXX Cally 20755 Holloway XXXXXXXXX Consumer PR 5365 Noble Nectar Island 725.0 2 Fitness 18.251453 -66.037056 Pacific Asia Bekasi Indonesia 20755 1/31/2018 22:56 77202 1360 13.110000 0.04 180517 327.75 0.29 1 327.75 314.640015 91.250000 Southeast Asia Java Occidental COMPLETE NaN 1360 73 NaN http://images.acmesports.sports/Smart+watch Smart watch 327.75 0 2/3/2018 22:56 Standard Class
1 TRANSFER 5 4 -249.089996 311.359985 Late delivery 1 73 Sporting Goods Caguas Puerto Rico XXXXXXXXX Irene 19492 Luna XXXXXXXXX Consumer PR 2679 Rustic Loop 725.0 2 Fitness 18.279451 -66.037064 Pacific Asia Bikaner India 19492 1/13/2018 12:27 75939 1360 16.389999 0.05 179254 327.75 -0.80 1 327.75 311.359985 -249.089996 South Asia Rajastán PENDING NaN 1360 73 NaN http://images.acmesports.sports/Smart+watch Smart watch 327.75 0 1/18/2018 12:27 Standard Class
2 CASH 4 4 -247.779999 309.720001 Shipping on time 0 73 Sporting Goods San Jose EE. UU. XXXXXXXXX Gillian 19491 Maldonado XXXXXXXXX Consumer CA 8510 Round Bear Gate 95125.0 2 Fitness 37.292233 -121.881279 Pacific Asia Bikaner India 19491 1/13/2018 12:06 75938 1360 18.030001 0.06 179253 327.75 -0.80 1 327.75 309.720001 -247.779999 South Asia Rajastán CLOSED NaN 1360 73 NaN http://images.acmesports.sports/Smart+watch Smart watch 327.75 0 1/17/2018 12:06 Standard Class
3 DEBIT 3 4 22.860001 304.809998 Advance shipping 0 73 Sporting Goods Los Angeles EE. UU. XXXXXXXXX Tana 19490 Tate XXXXXXXXX Home Office CA 3200 Amber Bend 90027.0 2 Fitness 34.125946 -118.291016 Pacific Asia Townsville Australia 19490 1/13/2018 11:45 75937 1360 22.940001 0.07 179252 327.75 0.08 1 327.75 304.809998 22.860001 Oceania Queensland COMPLETE NaN 1360 73 NaN http://images.acmesports.sports/Smart+watch Smart watch 327.75 0 1/16/2018 11:45 Standard Class
4 PAYMENT 2 4 134.210007 298.250000 Advance shipping 0 73 Sporting Goods Caguas Puerto Rico XXXXXXXXX Orli 19489 Hendricks XXXXXXXXX Corporate PR 8671 Iron Anchor Corners 725.0 2 Fitness 18.253769 -66.037048 Pacific Asia Townsville Australia 19489 1/13/2018 11:24 75936 1360 29.500000 0.09 179251 327.75 0.45 1 327.75 298.250000 134.210007 Oceania Queensland PENDING_PAYMENT NaN 1360 73 NaN http://images.acmesports.sports/Smart+watch Smart watch 327.75 0 1/15/2018 11:24 Standard Class
In [5]:
print(f"O dataframe possui {df.shape[0]} linhas e {df.shape[1]} colunas.")
O dataframe possui 180519 linhas e 53 colunas.
In [6]:
# Informações sobre todas as colunas do dataset
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 180519 entries, 0 to 180518
Data columns (total 53 columns):
 #   Column                         Non-Null Count   Dtype  
---  ------                         --------------   -----  
 0   Type                           180519 non-null  object 
 1   Days for shipping (real)       180519 non-null  int64  
 2   Days for shipment (scheduled)  180519 non-null  int64  
 3   Benefit per order              180519 non-null  float64
 4   Sales per customer             180519 non-null  float64
 5   Delivery Status                180519 non-null  object 
 6   Late_delivery_risk             180519 non-null  int64  
 7   Category Id                    180519 non-null  int64  
 8   Category Name                  180519 non-null  object 
 9   Customer City                  180519 non-null  object 
 10  Customer Country               180519 non-null  object 
 11  Customer Email                 180519 non-null  object 
 12  Customer Fname                 180519 non-null  object 
 13  Customer Id                    180519 non-null  int64  
 14  Customer Lname                 180511 non-null  object 
 15  Customer Password              180519 non-null  object 
 16  Customer Segment               180519 non-null  object 
 17  Customer State                 180519 non-null  object 
 18  Customer Street                180519 non-null  object 
 19  Customer Zipcode               180516 non-null  float64
 20  Department Id                  180519 non-null  int64  
 21  Department Name                180519 non-null  object 
 22  Latitude                       180519 non-null  float64
 23  Longitude                      180519 non-null  float64
 24  Market                         180519 non-null  object 
 25  Order City                     180519 non-null  object 
 26  Order Country                  180519 non-null  object 
 27  Order Customer Id              180519 non-null  int64  
 28  order date (DateOrders)        180519 non-null  object 
 29  Order Id                       180519 non-null  int64  
 30  Order Item Cardprod Id         180519 non-null  int64  
 31  Order Item Discount            180519 non-null  float64
 32  Order Item Discount Rate       180519 non-null  float64
 33  Order Item Id                  180519 non-null  int64  
 34  Order Item Product Price       180519 non-null  float64
 35  Order Item Profit Ratio        180519 non-null  float64
 36  Order Item Quantity            180519 non-null  int64  
 37  Sales                          180519 non-null  float64
 38  Order Item Total               180519 non-null  float64
 39  Order Profit Per Order         180519 non-null  float64
 40  Order Region                   180519 non-null  object 
 41  Order State                    180519 non-null  object 
 42  Order Status                   180519 non-null  object 
 43  Order Zipcode                  24840 non-null   float64
 44  Product Card Id                180519 non-null  int64  
 45  Product Category Id            180519 non-null  int64  
 46  Product Description            0 non-null       float64
 47  Product Image                  180519 non-null  object 
 48  Product Name                   180519 non-null  object 
 49  Product Price                  180519 non-null  float64
 50  Product Status                 180519 non-null  int64  
 51  shipping date (DateOrders)     180519 non-null  object 
 52  Shipping Mode                  180519 non-null  object 
dtypes: float64(15), int64(14), object(24)
memory usage: 73.0+ MB
In [7]:
numerics = ["int16", "int32", "int64", "float16", "float32", "float64"]

numericas = df.select_dtypes(include=numerics)

nao_numericas = df.select_dtypes(exclude=numerics)

print(f"Temos {numericas.shape[1]} colunas numéricas e {nao_numericas.shape[1]} colunas não-numéricas.")
Temos 29 colunas numéricas e 24 colunas não-numéricas.
In [8]:
# Estátísticas descritivas do dataset
df.describe()
Out[8]:
Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Late_delivery_risk Category Id Customer Id Customer Zipcode Department Id Latitude Longitude Order Customer Id Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Zipcode Product Card Id Product Category Id Product Description Product Price Product Status
count 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180516.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 180519.000000 24840.000000 180519.000000 180519.000000 0.0 180519.000000 180519.0
mean 3.497654 2.931847 21.974989 183.107609 0.548291 31.851451 6691.379495 35921.126914 5.443460 29.719955 -84.915675 6691.379495 36221.894903 692.509764 20.664741 0.101668 90260.000000 141.232550 0.120647 2.127638 203.772096 183.107609 21.974989 55426.132327 692.509764 31.851451 NaN 141.232550 0.0
std 1.623722 1.374449 104.433526 120.043670 0.497664 15.640064 4162.918106 37542.461122 1.629246 9.813646 21.433241 4162.918106 21045.379569 336.446807 21.800901 0.070415 52111.490959 139.732492 0.466796 1.453451 132.273077 120.043670 104.433526 31919.279101 336.446807 15.640064 NaN 139.732492 0.0
min 0.000000 0.000000 -4274.979980 7.490000 0.000000 2.000000 1.000000 603.000000 2.000000 -33.937553 -158.025986 1.000000 1.000000 19.000000 0.000000 0.000000 1.000000 9.990000 -2.750000 1.000000 9.990000 7.490000 -4274.979980 1040.000000 19.000000 2.000000 NaN 9.990000 0.0
25% 2.000000 2.000000 7.000000 104.379997 0.000000 18.000000 3258.500000 725.000000 4.000000 18.265432 -98.446312 3258.500000 18057.000000 403.000000 5.400000 0.040000 45130.500000 50.000000 0.080000 1.000000 119.980003 104.379997 7.000000 23464.000000 403.000000 18.000000 NaN 50.000000 0.0
50% 3.000000 4.000000 31.520000 163.990005 1.000000 29.000000 6457.000000 19380.000000 5.000000 33.144863 -76.847908 6457.000000 36140.000000 627.000000 14.000000 0.100000 90260.000000 59.990002 0.270000 1.000000 199.919998 163.990005 31.520000 59405.000000 627.000000 29.000000 NaN 59.990002 0.0
75% 5.000000 4.000000 64.800003 247.399994 1.000000 45.000000 9779.000000 78207.000000 7.000000 39.279617 -66.370583 9779.000000 54144.000000 1004.000000 29.990000 0.160000 135389.500000 199.990005 0.360000 3.000000 299.950012 247.399994 64.800003 90008.000000 1004.000000 45.000000 NaN 199.990005 0.0
max 6.000000 4.000000 911.799988 1939.989990 1.000000 76.000000 20757.000000 99205.000000 12.000000 48.781933 115.263077 20757.000000 77204.000000 1363.000000 500.000000 0.250000 180519.000000 1999.989990 0.500000 5.000000 1999.989990 1939.989990 911.799988 99301.000000 1363.000000 76.000000 NaN 1999.989990 0.0

3.1 Detecção de Outliers¶

Esta etapa consiste em detectar a quantidade de outliers presente em cada uma das variáveis da base de dados. São calculados utilizando Q1 (primeiro quartil), Q3 (terceiro quartil) e IQR (Distância Interquartil):

Q1: Este é o valor que separa os 25% menores valores da coluna. Também é conhecido como primeiro quartil.

Q3: Este é o valor que separa os 25% maiores valores da coluna. Também é conhecido como terceiro quartil.

$$IQR = Q3 - Q1$$
Será considerado Outlier quando o valor da variável (VAR):

$$VAR > Q3 + 1.5IQR$$ $$VAR < Q1 - 1.5IQR$$

In [9]:
# Função para identificar outliers em uma variável
def detect_outliers(column):
    Q1 = column.quantile(0.25)
    Q3 = column.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = column[(column < lower_bound) | (column > upper_bound)]
    return outliers

# Dicionário para armazenar os outliers de cada variável
outliers_dict = {}

# Loop através das variáveis numéricas
for col in df.select_dtypes(include=['float64', 'int64']):
    outliers = detect_outliers(df[col])
    outliers_dict[col] = outliers

# Crie um DataFrame com os outliers
outliers_df = pd.DataFrame(outliers_dict)

# Conte quantos outliers cada variável possui
contagem_outliers = outliers_df.count()

# Exiba a contagem de outliers para cada variável
contagem_outliers_df = pd.DataFrame({'Variavel': contagem_outliers.index, 'Quantidade de Outliers': contagem_outliers.values})
contagem_outliers_df
Out[9]:
Variavel Quantidade de Outliers
0 Days for shipping (real) 0
1 Days for shipment (scheduled) 0
2 Benefit per order 18942
3 Sales per customer 1943
4 Late_delivery_risk 0
5 Category Id 0
6 Customer Id 1198
7 Customer Zipcode 0
8 Department Id 362
9 Latitude 9
10 Longitude 1414
11 Order Customer Id 1198
12 Order Id 0
13 Order Item Cardprod Id 0
14 Order Item Discount 7537
15 Order Item Discount Rate 0
16 Order Item Id 0
17 Order Item Product Price 2048
18 Order Item Profit Ratio 17300
19 Order Item Quantity 0
20 Sales 488
21 Order Item Total 1943
22 Order Profit Per Order 18942
23 Order Zipcode 0
24 Product Card Id 0
25 Product Category Id 0
26 Product Description 0
27 Product Price 2048
28 Product Status 0

3.2 Valores Nulos¶

In [10]:
# Fazendo a soma de valores nulos por coluna
df.isnull().sum()
Out[10]:
Type                                  0
Days for shipping (real)              0
Days for shipment (scheduled)         0
Benefit per order                     0
Sales per customer                    0
Delivery Status                       0
Late_delivery_risk                    0
Category Id                           0
Category Name                         0
Customer City                         0
Customer Country                      0
Customer Email                        0
Customer Fname                        0
Customer Id                           0
Customer Lname                        8
Customer Password                     0
Customer Segment                      0
Customer State                        0
Customer Street                       0
Customer Zipcode                      3
Department Id                         0
Department Name                       0
Latitude                              0
Longitude                             0
Market                                0
Order City                            0
Order Country                         0
Order Customer Id                     0
order date (DateOrders)               0
Order Id                              0
Order Item Cardprod Id                0
Order Item Discount                   0
Order Item Discount Rate              0
Order Item Id                         0
Order Item Product Price              0
Order Item Profit Ratio               0
Order Item Quantity                   0
Sales                                 0
Order Item Total                      0
Order Profit Per Order                0
Order Region                          0
Order State                           0
Order Status                          0
Order Zipcode                    155679
Product Card Id                       0
Product Category Id                   0
Product Description              180519
Product Image                         0
Product Name                          0
Product Price                         0
Product Status                        0
shipping date (DateOrders)            0
Shipping Mode                         0
dtype: int64
In [11]:
# visualizando as colunas numéricas com dados faltantes
missingno.matrix(numericas,figsize=(40,10))
Out[11]:
<Axes: >
No description has been provided for this image

Pela imagem acima, verifica-se que há duas colunas numéricas com grande quantidade de valores nulos: Order Zipcode e Product Description. pela contagem anterior, sabe-se que uma terceira coluna (Customer Zipcode) possui apenas 3 valores nulos, o que não foi possível verificar visualmente devido à baixa quantidade.

In [12]:
# visualizando as colunas não numéricas com dados faltantes
missingno.matrix(nao_numericas,figsize=(40,10))
Out[12]:
<Axes: >
No description has been provided for this image

Pela imagem acima, não é possível identificar visualmente alguma variável não numérica com valores nulos. Porém, sabe se a variável Customer Lname possui 8 valores nulos em sua composição, não sendo possível verificar visualmente por ser uma quantidade pequena perante o número de registros da base.

In [13]:
# fazendo unpack de linhas e colunas
rows, columns = df.shape

# Percentual de dados faltantes por coluna
percentual_nan = ((df.isnull().sum()/rows) * 100).round(2)
percentual_nan
Out[13]:
Type                               0.00
Days for shipping (real)           0.00
Days for shipment (scheduled)      0.00
Benefit per order                  0.00
Sales per customer                 0.00
Delivery Status                    0.00
Late_delivery_risk                 0.00
Category Id                        0.00
Category Name                      0.00
Customer City                      0.00
Customer Country                   0.00
Customer Email                     0.00
Customer Fname                     0.00
Customer Id                        0.00
Customer Lname                     0.00
Customer Password                  0.00
Customer Segment                   0.00
Customer State                     0.00
Customer Street                    0.00
Customer Zipcode                   0.00
Department Id                      0.00
Department Name                    0.00
Latitude                           0.00
Longitude                          0.00
Market                             0.00
Order City                         0.00
Order Country                      0.00
Order Customer Id                  0.00
order date (DateOrders)            0.00
Order Id                           0.00
Order Item Cardprod Id             0.00
Order Item Discount                0.00
Order Item Discount Rate           0.00
Order Item Id                      0.00
Order Item Product Price           0.00
Order Item Profit Ratio            0.00
Order Item Quantity                0.00
Sales                              0.00
Order Item Total                   0.00
Order Profit Per Order             0.00
Order Region                       0.00
Order State                        0.00
Order Status                       0.00
Order Zipcode                     86.24
Product Card Id                    0.00
Product Category Id                0.00
Product Description              100.00
Product Image                      0.00
Product Name                       0.00
Product Price                      0.00
Product Status                     0.00
shipping date (DateOrders)         0.00
Shipping Mode                      0.00
dtype: float64

3.3 Linhas Duplicadas¶

In [14]:
# verificando se há linhas duplicadas
df[df.duplicated()]
Out[14]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode

Nota-se que o dataset contem 180.519 entradas com informações de vendas de produtos da empresa SupplyAll e que todos os registros são únicos. Desta forma, é possível concluir que não existem entradas duplicadas.

3.4 Valores Diferentes por Coluna¶

In [15]:
# Definindo quantos valores diferentes existem em cada variável

contagem = pd.DataFrame(columns=['Variavel','Contagens_Distintas'])

for coluna in df.columns:
    dados = pd.DataFrame({'Variavel': [coluna], 'Contagens_Distintas': [df[coluna].value_counts().shape[0]]})
    contagem = pd.concat([contagem, dados], ignore_index = True)

contagem
Out[15]:
Variavel Contagens_Distintas
0 Type 4
1 Days for shipping (real) 7
2 Days for shipment (scheduled) 4
3 Benefit per order 21998
4 Sales per customer 2927
5 Delivery Status 4
6 Late_delivery_risk 2
7 Category Id 51
8 Category Name 50
9 Customer City 563
10 Customer Country 2
11 Customer Email 1
12 Customer Fname 782
13 Customer Id 20652
14 Customer Lname 1109
15 Customer Password 1
16 Customer Segment 3
17 Customer State 46
18 Customer Street 7458
19 Customer Zipcode 995
20 Department Id 11
21 Department Name 11
22 Latitude 11250
23 Longitude 4487
24 Market 5
25 Order City 3597
26 Order Country 164
27 Order Customer Id 20652
28 order date (DateOrders) 65752
29 Order Id 65752
30 Order Item Cardprod Id 118
31 Order Item Discount 1017
32 Order Item Discount Rate 18
33 Order Item Id 180519
34 Order Item Product Price 75
35 Order Item Profit Ratio 162
36 Order Item Quantity 5
37 Sales 193
38 Order Item Total 2927
39 Order Profit Per Order 21998
40 Order Region 23
41 Order State 1089
42 Order Status 9
43 Order Zipcode 609
44 Product Card Id 118
45 Product Category Id 51
46 Product Description 0
47 Product Image 118
48 Product Name 118
49 Product Price 75
50 Product Status 1
51 shipping date (DateOrders) 63701
52 Shipping Mode 4

Com a informação das contagens distintas de cada variável, podemos obter as seguintes informações:

  • Os clientes pagaram os produtos através de 4 meios de pagamentos diferentes
  • Há 9 status diferentes para os pedidos
  • Há 4 status diferentes para as entregas dos produtos
  • Os produtos são entregues para 5 grandes regiões diferentes do mundo
  • Os produtos foram enviados para 164 países diferentes
  • Os clientes são de 2 países diferentes
  • Há 51 categorias de produtos diferentes
  • Há 3 segmentos distintos de clientes
  • Há 20.652 clientes no total
  • Há 23 micro regiões do mundo onde os produtos são entregues
In [16]:
# Desconsiderar pedidos que tiveram entrega cancelada. Está sendo levado em consideração que entregas canceladas têm o dinheiro
# da compra enviado de volta ao cliente, não contribuindo para os cofres da companhia
df_filtrado = df[df['Delivery Status']!='Shipping canceled']

4. Avaliação dos clientes

In [17]:
# Transformando a variável 'order date (DateOrders)' em datetime para trabalhar com datas
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])

# Extraindo o ano
df['Ano'] = df['order date (DateOrders)'].dt.year

# Agrupando por Customer_Id e Year, e contando os Order_Id únicos
compras_por_cliente_ano = df.groupby(['Customer Id', 'Ano'])['Order Id'].nunique().reset_index()

# Renomeando a coluna para refletir a contagem
compras_por_cliente_ano.rename(columns={'Order Id': 'Quantidade de Pedidos'}, inplace=True)

compras_por_cliente_ano_pivot = compras_por_cliente_ano.pivot_table(index="Customer Id",columns="Ano",values="Quantidade de Pedidos",fill_value=0)

compras_por_cliente_ano_pivot
Out[17]:
Ano 2015 2016 2017 2018
Customer Id
1 1.0 0.0 0.0 0.0
2 1.0 1.0 2.0 0.0
3 1.0 1.0 3.0 0.0
4 2.0 1.0 1.0 0.0
5 0.0 3.0 0.0 0.0
... ... ... ... ...
20753 0.0 0.0 0.0 1.0
20754 0.0 0.0 0.0 1.0
20755 0.0 0.0 0.0 1.0
20756 0.0 0.0 0.0 1.0
20757 0.0 0.0 0.0 1.0

20652 rows × 4 columns

In [18]:
# Filtrando todos os clientes que compraram em 2018
compras_por_cliente_ano_pivot[compras_por_cliente_ano_pivot[2018] > 0].value_counts()
Out[18]:
2015  2016  2017  2018
0.0   0.0   0.0   1.0     2123
Name: count, dtype: int64

Chega-se a conclusão que, todos os clientes que compraram em 2018, compraram apenas em 2018!

In [19]:
# Filtrando para trazer somente a informação do ano de 2018
compras_2018 = compras_por_cliente_ano[compras_por_cliente_ano['Ano'] == 2018]

# Verificando se há clientes que compraram mais de uma vez em 2018
clientes_compras_multiplos_2018 = compras_2018[compras_2018['Quantidade de Pedidos'] > 1]
clientes_compras_multiplos_2018
Out[19]:
Customer Id Ano Quantidade de Pedidos

Observa-se que, todos os clientes que compraram em 2018, compraram apenas em 2018 e apenas uma vez! Para verificar se este comportamento atípico começa antes de 2018, será analisado também o ano de 2017.

In [20]:
# Filtrando para o ano de 2017
compras_2017 = compras_por_cliente_ano[compras_por_cliente_ano['Ano'] == 2017]

# Certificando-se de que os IDs estão em ordem crescente
compras_2017 = compras_2017.sort_values(by='Customer Id')

# Encontrar a partir de qual Customer_Id todos os seguintes compraram apenas uma vez
starting_customer_id = None

for i in range(len(compras_2017)):
    current_id = compras_2017.iloc[i]['Customer Id']
    current_purchases = compras_2017.iloc[i]['Quantidade de Pedidos']

    if current_purchases != 1:
        continue  # Pula se o cliente atual fez mais de uma compra

    # Verifica se todos os IDs seguintes estão em sequência e têm apenas uma compra
    if all((compras_2017.iloc[j]['Customer Id'] == current_id + j - i) and
           (compras_2017.iloc[j]['Quantidade de Pedidos'] == 1) for j in range(i, len(compras_2017))):
        starting_customer_id = current_id
        break

if starting_customer_id is not None:
    print(f"Todos os Customer_Id a partir de {starting_customer_id} fizeram apenas uma compra em 2017.")
else:
    print("Não há uma sequência contínua de clientes com apenas uma compra.")
Todos os Customer_Id a partir de 12440 fizeram apenas uma compra em 2017.

Sendo assim, entre todos os Custumer_Id a partir de 12440, foi verificado a partir de que data eles começaram a comprar.

In [21]:
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])

# Filtrando 'Customer Id' a partir de 12440
df_customer_filtrado = df[df['Customer Id'] >= 12440]

# Obtendo a data mínima
data_minima = df_customer_filtrado['order date (DateOrders)'].min()

print("Data mínima para Customer Id a partir de 12440:", data_minima)
Data mínima para Customer Id a partir de 12440: 2017-10-02 13:50:00
In [22]:
df[df['Customer Id'] == 12440]
Out[22]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode Ano
24764 DEBIT 2 1 1.98 26.42 Late delivery 1 59 Books San Marcos EE. UU. XXXXXXXXX Joanha 12440 Mirkckociv XXXXXXXXX Corporate CA 8324 Little Common 92069.0 8 Book Shop 33.146751 -117.169533 Europe Forst Alemania 12440 2017-10-02 13:50:00 68887 1346 4.66 0.15 172202 31.08 0.08 1 31.08 26.42 1.98 Western Europe Brandenburgo COMPLETE NaN 1346 59 NaN http://images.acmesports.sports/Sports+Books Sports Books 31.08 0 10/4/2017 13:50 First Class 2017
In [23]:
df[(df['Customer Id'] < 12440) & (df['order date (DateOrders)'] >= data_minima)]
Out[23]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode Ano

Conclusão: A partir de 2017-10-02, todos os clientes são novos e compraram apenas uma vez. Sendo assim, para não trabalhar com uma base de dados com uma possível interferência (erro operacional), o conjunto de dados considerará dos dados até setembro/2017.

In [24]:
# Atualizando os dataframes df_filtrado e df com o intervalo de datas correto, para seguir com as análises
df = pd.read_csv('DataCoSupplyChainDataset.csv', encoding='ISO-8859-1')

# Transformando a coluna de data para o formato data e hora
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])

# Definindo a data limite para ser considerada na análise
data_limite = pd.Timestamp('2017-09-30 23:59:59')

# Filtrando o dataframe df para conter datas menores ou iguais a que 2017-09-30
df = df[df['order date (DateOrders)'] <= data_limite]

# Desconsiderar pedidos que tiveram entrega cancelada
df_filtrado = df[df['Delivery Status']!='Shipping canceled']

5. Relações entre variáveis

In [25]:
numericas = df_filtrado.select_dtypes(include=numerics)

# Remover colunas específicas
numericas_filtradas = numericas.drop(['Category Id', 'Customer Id', 'Customer Zipcode', 'Department Id', 'Latitude',
                            'Longitude', 'Order Customer Id', 'Order Id', 'Order Item Cardprod Id', 'Order Item Id',
                            'Order Zipcode', 'Product Card Id', 'Product Category Id', 'Product Description',
                            'Product Status'], axis=1)

# Calcular a matriz de correlação
corr_matrix = numericas_filtradas.corr()

# Criar o heatmap
plt.figure(figsize=(16, 9))
sns.heatmap(corr_matrix, annot=True, fmt=".2f", cmap='RdBu')
plt.title('Correlação entre variáveis', fontsize=16)
plt.show()
No description has been provided for this image

Variáveis altamente correlacionadas¶

  • Benefit per order e Order Profit Per Order (correlação 1.00)
  • Sales per customer e Sales (correlação 0.99)
  • Sales per Customer e Order Item Total (correlação 1.00)
  • Order Item Product Price e Product Price (correlação 1.00)

Obs: É importante lembrar que a correlação não implica causalidade. Mesmo quando duas variáveis estão correlacionadas, isso não significa necessariamente que uma causa a outra. Outros fatores ou variáveis não incluídas na análise podem estar contribuindo para a relação observada. Portanto, a interpretação da correlação deve ser feita com cuidado e, quando necessário, devem ser realizadas análises adicionais para entender melhor a relação entre as variáveis.

6. Avaliação de vendas e receitas

¶

6.1 Análise temporal das vendas - quantidade¶

In [26]:
# Calcula a quantidade de vendas por mes
vendas_por_mes = df_filtrado.resample('M', on='order date (DateOrders)').size()

# Criando um dataframe a partir da série
vendas_por_mes = vendas_por_mes.reset_index()

vendas_por_mes.rename(columns={0: 'Vendas (em quantidade)'}, inplace=True)

# Calculando a variação das vendas
vendas_por_mes['Variação de Vendas'] = vendas_por_mes['Vendas (em quantidade)'].diff()

# Calculando a variação percentual das vendas
vendas_por_mes['Variação Percentual'] = vendas_por_mes['Vendas (em quantidade)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
vendas_por_mes['Variação Percentual'] = (vendas_por_mes['Variação Percentual'] * 100).round(2)

# Visualização
fig = px.line(vendas_por_mes, x='order date (DateOrders)', y='Vendas (em quantidade)', 
              title='Vendas totais por mês (em quantidade)', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=600)
fig.show()
In [27]:
vendas_por_mes['Vendas (em quantidade)'].describe()
Out[27]:
count      33.000000
mean     4987.030303
std       159.145469
min      4487.000000
25%      4926.000000
50%      5025.000000
75%      5105.000000
max      5206.000000
Name: Vendas (em quantidade), dtype: float64
In [28]:
# Determinando outliers

Q1 = vendas_por_mes['Vendas (em quantidade)'].describe()[4]
Q3 = vendas_por_mes['Vendas (em quantidade)'].describe()[6]
IQR = Q3 - Q1

Limite_superior = Q3 + 1.5*IQR
Limite_inferior = Q1 - 1.5*IQR

print(f'Quantidade de vendas mensais maiores que {Limite_superior} são outliers superiores')
print(f'Quantidade de vendas mensais menores que {Limite_inferior} são outliers inferiores')
Quantidade de vendas mensais maiores que 5373.5 são outliers superiores
Quantidade de vendas mensais menores que 4657.5 são outliers inferiores

Ou seja, há apenas 1 outlier (inferior), em fevereiro/2015, quando foram vendidas 4487 itens.

In [29]:
# Visualizando a distribuição da quantidade de vendas ao mês
plt.figure(figsize=(16, 9))
sns.histplot(vendas_por_mes['Vendas (em quantidade)'], bins=50, kde=True)
plt.title('Distribuição da quantidade de vendas')
plt.xlabel('Quantidade')
plt.ylabel('Frequência')
plt.show()
No description has been provided for this image

Em todo o período avaliado, a média de vendas ao mês está em torno de 4987, bem próxima da mediana que está em 5025. Ou seja, A quantidade de vendas está bem concentrada em torno da média, com um baixo desvio padrão. Como a média é maior que a mediana, trata-se de uma distribuição assimétrica à esquerda.

In [30]:
print(f"A assimetria da distribuição de quantidades assume valor: {skew(vendas_por_mes['Vendas (em quantidade)']):.2f}")
A assimetria da distribuição de quantidades assume valor: -1.32
In [31]:
# Visualização
fig = px.line(vendas_por_mes, x='order date (DateOrders)', y='Variação de Vendas', 
              title='Variação das vendas mensais - em quantidade', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=600)
fig.show()

Pelos gráficos acima, percebe-se que o mês de fevereiro é o pior mês para vendas, com as maiores variações negativas.

In [32]:
# Agrupando pelo dia da semana e contando as vendas
vendas_dia_da_semana = df_filtrado.groupby(df_filtrado['order date (DateOrders)'].dt.day_name()).size()

# Ordenar os resultados pelos dias da semana
ordem_semana = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
vendas_dia_da_semana = vendas_dia_da_semana.reindex(ordem_semana)

vendas_dia_da_semana = vendas_dia_da_semana.reset_index()
vendas_dia_da_semana.rename(columns={0:'Quantidade de Vendas', 'order date (DateOrders)':'Dia da Semana'}, inplace=True)

vendas_dia_da_semana
Out[32]:
Dia da Semana Quantidade de Vendas
0 Monday 23336
1 Tuesday 23409
2 Wednesday 23272
3 Thursday 23598
4 Friday 23755
5 Saturday 23640
6 Sunday 23562
In [33]:
fig = px.bar(vendas_dia_da_semana, x='Dia da Semana', y='Quantidade de Vendas', color = 'Dia da Semana',
             text = 'Quantidade de Vendas', title='Vendas por dia da semana - em quantidade')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Percebe-se que as quantidade de vendas por dia da semana assumem valores gerais bem próximos uns dos outros, não tendo um dia de preferencial de compras dos clientes.

6.2 Análise temporal das vendas - financeiro¶

In [34]:
# Agrupar os dados por ano e mês e somar as vendas
sales_over_time = df_filtrado.resample('M', on='order date (DateOrders)').sum()['Sales']

# Criando um DataFrame a partir da série
sales_over_time_df = sales_over_time.reset_index()

# Calculando a variação das vendas
sales_over_time_df['Variação de Vendas'] = sales_over_time_df['Sales'].diff()

# Calculando a variação percentual das vendas
sales_over_time_df['Variação Percentual'] = sales_over_time_df['Sales'].pct_change()

# Convertendo a variação para porcentagem e arredondando
sales_over_time_df['Variação Percentual'] = (sales_over_time_df['Variação Percentual'] * 100).round(2)


# Visualização
fig = px.line(sales_over_time_df, x='order date (DateOrders)', y='Sales', 
              title='Vendas Totais por Mês (em milhões)', labels={'order date (DateOrders)': 'Data', 'Sales': 'Vendas Totais (em milhões)'})
fig.update_layout(width=1000, height=600)
fig.show()
In [35]:
sales_over_time.describe()
Out[35]:
count    3.300000e+01
mean     9.935833e+05
std      3.796040e+04
min      8.816670e+05
25%      9.784447e+05
50%      9.924190e+05
75%      1.007894e+06
max      1.088272e+06
Name: Sales, dtype: float64

As vendas variam em torno de uma média de 993 mil, terminando setembro/2017 na maior alta histórica, cerca de 1,08MM entre todos os produtos comercializados.

In [36]:
# Visualização
fig = px.line(sales_over_time_df, x='order date (DateOrders)', y='Variação de Vendas', 
              title='Variação das vendas Totais por Mês', labels={'order date (DateOrders)': 'Data', 'Variação de Vendas':'Variação de Vendas (em milhares)'})
fig.update_layout(width=1000, height=600)
fig.show()
In [37]:
sales_over_time_df[abs(sales_over_time_df['Variação Percentual']) >= 5]
Out[37]:
order date (DateOrders) Sales Variação de Vendas Variação Percentual
1 2015-02-28 8.816670e+05 -127187.442220 -12.61
2 2015-03-31 1.007758e+06 126091.062438 14.30
13 2016-02-29 9.283721e+05 -79433.621397 -7.88
14 2016-03-31 9.879204e+05 59548.221787 6.41
26 2017-03-31 1.007894e+06 59852.532387 6.31
28 2017-05-31 1.047041e+06 55507.750888 5.60
29 2017-06-30 9.857683e+05 -61272.661324 -5.85
30 2017-07-31 1.047375e+06 61607.130510 6.25

As vendas oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Março/2015: 14,30%,
  • Março/2016: 6,41%
  • Março/2017: 6,31%

Destaque para os meses de variação negativa nas vendas:¶

  • Fevereiro/2015: -12,61%,
  • Fevereiro/2016: -7,88%
  • Junho/2017: -5,85%

Vendas por segmento de cliente¶

In [38]:
# Agrupando por 'Category Name' e somando as vendas
Venda_por_segmento = df_filtrado.groupby('Customer Segment')['Sales'].sum().reset_index()

Venda_por_segmento['Sales'] = (Venda_por_segmento['Sales'] / 1000000).round(2)

Venda_por_segmento.rename(columns={'Sales': 'Venda (em milhões)', 'Customer Segment': 'Segmento de Cliente'}, inplace=True)

# Ordenando os resultados
Venda_por_segmento = Venda_por_segmento.sort_values(by='Venda (em milhões)', ascending=False).reset_index(drop=True)

Venda_por_segmento
Out[38]:
Segmento de Cliente Venda (em milhões)
0 Consumer 17.00
1 Corporate 9.96
2 Home Office 5.83
In [39]:
fig = px.bar(Venda_por_segmento, x='Segmento de Cliente', y='Venda (em milhões)', color = 'Segmento de Cliente',
             text = 'Venda (em milhões)', title='Venda por segmento de cliente (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
In [40]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Venda_por_segmento_e_data = df_filtrado.groupby(['Customer Segment', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Venda_por_segmento_e_data['Sales'] = (Venda_por_segmento_e_data['Sales'] / 1000).round(2)
Venda_por_segmento_e_data.rename(columns={'Customer Segment':'Segmento', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação das vendas por segmento
Venda_por_segmento_e_data['Variação de Vendas'] = Venda_por_segmento_e_data.groupby('Segmento')['Vendas (em milhares)'].diff()

# Calculando a variação percentual das vendas por segmento
Venda_por_segmento_e_data['Variação Percentual'] = Venda_por_segmento_e_data.groupby('Segmento')['Vendas (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Venda_por_segmento_e_data['Variação Percentual'] = (Venda_por_segmento_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Venda_por_segmento_e_data, x='Data', y='Vendas (em milhares)', color='Segmento', title='Venda por segmento de cliente ao longo do tempo (em milhares)')

fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
In [41]:
# Criar o gráfico usando Plotly
fig = px.line(Venda_por_segmento_e_data, x='Data', y='Variação de Vendas', color='Segmento', title='Variação de venda por segmento de cliente ao longo do tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [42]:
# Destacando os segmentos e os meses que tiveram uma variação maior que 10% nas vendas
Venda_por_segmento_e_data[abs(Venda_por_segmento_e_data['Variação Percentual']) >= 10]
Out[42]:
Segmento Data Vendas (em milhares) Variação de Vendas Variação Percentual
1 Consumer 2015-02-28 453.84 -73.70 -13.97
2 Consumer 2015-03-31 523.52 69.68 15.35
25 Consumer 2017-02-28 467.99 -62.99 -11.86
30 Consumer 2017-07-31 561.07 76.60 15.81
34 Corporate 2015-02-28 259.73 -40.18 -13.40
35 Corporate 2015-03-31 307.75 48.02 18.49
37 Corporate 2015-05-31 319.06 36.91 13.08
44 Corporate 2015-12-31 326.57 35.23 12.09
46 Corporate 2016-02-29 272.38 -50.38 -15.61
60 Corporate 2017-04-30 284.89 -36.27 -11.29
61 Corporate 2017-05-31 336.20 51.31 18.01
75 Home Office 2015-10-31 186.21 17.97 10.68
81 Home Office 2016-04-30 151.36 -30.43 -16.74
82 Home Office 2016-05-31 168.05 16.69 11.03
92 Home Office 2017-03-31 197.60 28.14 16.61
96 Home Office 2017-07-31 168.52 -29.45 -14.88
97 Home Office 2017-08-31 186.82 18.30 10.86

As vendas por seguimento oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Corporate - Março/2015: 18,49%,
  • Home Office - Março/2017: 16,61%
  • Corporate - Março/2017: 18,01%

Destaque para os meses de variação negativa nas vendas:¶

  • Home Office - Abril/2016: -16,74%
  • Corporate - Fevereiro/2016: -15,61%
  • Consumer - Fevereiro/2015: -13,97%

Categorias com mais vendas¶

In [43]:
# Agrupando por 'Category Name' e somando as vendas
vendas_por_categoria = df_filtrado.groupby('Category Name')['Sales'].sum().reset_index()

vendas_por_categoria['Sales'] = (vendas_por_categoria['Sales'] / 1000000).round(3)

vendas_por_categoria.rename(columns={'Sales': 'Vendas (em milhões)', 'Category Name': 'Nome da Categoria'}, inplace=True)

# Ordenando os resultados
vendas_por_categoria = vendas_por_categoria.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)

#sales_by_category_sorted = sales_by_category_sorted.head(10)

vendas_por_categoria
Out[43]:
Nome da Categoria Vendas (em milhões)
0 Fishing 6.632
1 Cleats 4.237
2 Camping & Hiking 3.942
3 Cardio Equipment 3.527
4 Women's Apparel 3.008
5 Water Sports 2.977
6 Indoor/Outdoor Games 2.762
7 Men's Footwear 2.760
8 Shop By Sport 1.252
9 Electronics 0.357
10 Girls' Apparel 0.143
11 Accessories 0.128
12 Golf Gloves 0.112
13 Golf Shoes 0.104
14 Baseball & Softball 0.091
15 Kids' Golf Clubs 0.089
16 Boxing & MMA 0.082
17 Golf Balls 0.073
18 Trade-In 0.066
19 Hunting & Shooting 0.055
20 Men's Golf Clubs 0.046
21 Hockey 0.046
22 Tennis & Racquet 0.043
23 Women's Golf Clubs 0.042
24 Lacrosse 0.038
25 Strength Training 0.037
26 Golf Apparel 0.034
27 Fitness Accessories 0.033
28 Soccer 0.026
29 As Seen on TV! 0.020
30 Basketball 0.018
31 Golf Bags & Carts 0.010
In [44]:
fig = px.bar(vendas_por_categoria.head(10), x='Nome da Categoria', y='Vendas (em milhões)', color = 'Nome da Categoria',
             text = 'Vendas (em milhões)', title='Top 10 categorias com mais vendas (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
In [45]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_categoria_e_data = df_filtrado.groupby(['Category Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Vendas_por_categoria_e_data['Sales'] = (Vendas_por_categoria_e_data['Sales'] / 1000).round(2)
Vendas_por_categoria_e_data.rename(columns={'Category Name':'Categoria', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação das vendas por categoria
Vendas_por_categoria_e_data['Variação de Vendas'] = Vendas_por_categoria_e_data.groupby('Categoria')['Vendas (em milhares)'].diff()

# Calculando a variação percentual das vendas por categoria
Vendas_por_categoria_e_data['Variação Percentual'] = Vendas_por_categoria_e_data.groupby('Categoria')['Vendas (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Vendas_por_categoria_e_data['Variação Percentual'] = (Vendas_por_categoria_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_categoria_e_data, x='Data', y='Vendas (em milhares)', color='Categoria', title='Vendas por categoria ao longo do tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [46]:
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_categoria_e_data, x='Data', y='Variação de Vendas', color='Categoria', title='Variação das vendas por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [47]:
# Destacando as categorias e os meses que tiveram uma variação maior que 500% nas vendas
Vendas_por_categoria_e_data[abs(Vendas_por_categoria_e_data['Variação Percentual']) > 500.00]
Out[47]:
Categoria Data Vendas (em milhares) Variação de Vendas Variação Percentual
289 Fitness Accessories 2017-09-30 4.20 3.92 1400.00
478 Hockey 2017-09-30 8.25 7.61 1189.06
667 Strength Training 2017-09-30 25.37 21.40 539.04

As vendas por categoria oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Fitness Accessories - Setembro/2017: 1400%,
  • Hockey - Setembro/2017: 1189,06%
  • Strength Training - Setembro/2017: 539,04%

Vendas por departamento¶

In [48]:
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_depart = df_filtrado.groupby('Department Name')['Sales'].sum().reset_index()

Vendas_por_depart['Sales'] = (Vendas_por_depart['Sales'] / 1000000).round(2)

Vendas_por_depart.rename(columns={'Sales': 'Vendas (em milhões)', 'Department Name': 'Departamento'}, inplace=True)

# Ordenando os resultados
Vendas_por_depart = Vendas_por_depart.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)

Vendas_por_depart
Out[48]:
Departamento Vendas (em milhões)
0 Fan Shop 16.37
1 Apparel 7.00
2 Golf 4.40
3 Footwear 3.81
4 Outdoors 0.95
5 Fitness 0.26
In [49]:
fig = px.bar(Vendas_por_depart, x='Departamento', y='Vendas (em milhões)', color = 'Departamento',
             text = 'Vendas (em milhões)', title='Vendas por departamento (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
In [50]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_depart_e_data = df_filtrado.groupby(['Department Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Vendas_por_depart_e_data['Sales'] = (Vendas_por_depart_e_data['Sales'] / 1000).round(2)
Vendas_por_depart_e_data.rename(columns={'Department Name':'Departamento', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação das vendas por departamento
Vendas_por_depart_e_data['Variação de Vendas'] = Vendas_por_depart_e_data.groupby('Departamento')['Vendas (em milhares)'].diff()

# Calculando a variação percentual das vendas por departamento
Vendas_por_depart_e_data['Variação Percentual'] = Vendas_por_depart_e_data.groupby('Departamento')['Vendas (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Vendas_por_depart_e_data['Variação Percentual'] = (Vendas_por_depart_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_depart_e_data, x='Data', y='Vendas (em milhares)', color='Departamento', title='Vendas por departamento ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [51]:
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_depart_e_data, x='Data', y='Variação de Vendas', color='Departamento', title='Variação das vendas por departamento ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [52]:
# Destacando os departamentos e os meses que tiveram uma variação maior que 50% nas vendas
Vendas_por_depart_e_data[abs(Vendas_por_depart_e_data['Variação Percentual']) > 50.00]
Out[52]:
Departamento Data Vendas (em milhares) Variação de Vendas Variação Percentual
77 Fitness 2015-12-31 7.83 2.63 50.58
84 Fitness 2016-07-31 8.87 3.61 68.63
94 Fitness 2017-05-31 13.82 6.29 83.53
98 Fitness 2017-09-30 30.07 19.21 176.89
193 Outdoors 2017-05-31 62.28 32.88 111.84

As vendas por departamento oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Fitness - Setembro/2017: 176,89%,
  • Outdoors - Maio/2017: 111,84%

Vendas por Mercado Global¶

In [53]:
# Agrupando por 'Category Name' e somando as vendas
vendas_por_market = df_filtrado.groupby('Market')['Sales'].sum().reset_index()

vendas_por_market['Sales'] = (vendas_por_market['Sales'] / 1000000).round(2)
vendas_por_market.rename(columns={'Sales': 'Vendas (em milhões)', 'Market': 'Mercado Global'}, inplace=True)

# Ordenando os resultados
vendas_por_market = vendas_por_market.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)

vendas_por_market
Out[53]:
Mercado Global Vendas (em milhões)
0 LATAM 9.82
1 Europe 9.19
2 Pacific Asia 6.74
3 USCA 4.84
4 Africa 2.21
In [54]:
fig = px.bar(vendas_por_market, x='Mercado Global', y='Vendas (em milhões)', color = 'Mercado Global',
             text = 'Vendas (em milhões)', title='Total em vendas por Mercado Global (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
In [55]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_market_e_data = df_filtrado.groupby(['Market', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Vendas_por_market_e_data['Sales'] = (Vendas_por_market_e_data['Sales'] / 1000000).round(2)
Vendas_por_market_e_data.rename(columns={'Market':'Mercado Global', 'Sales': 'Vendas (em milhões)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação das vendas por mercado global
Vendas_por_market_e_data['Variação de Vendas'] = Vendas_por_market_e_data.groupby('Mercado Global')['Vendas (em milhões)'].diff()

# Calculando a variação percentual das vendas por mercado global
Vendas_por_market_e_data['Variação Percentual'] = Vendas_por_market_e_data.groupby('Mercado Global')['Vendas (em milhões)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Vendas_por_market_e_data['Variação Percentual'] = (Vendas_por_market_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_market_e_data, x='Data', y='Vendas (em milhões)', color='Mercado Global', title='Vendas por Mercado Global ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [56]:
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_market_e_data, x='Data', y='Variação de Vendas', color='Mercado Global', title='Variação de vendas por Mercado Global ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [57]:
# Destacando os mercados globais e os meses que tiveram uma variação maior que 200% nas vendas
Vendas_por_market_e_data[abs(Vendas_por_market_e_data['Variação Percentual']) > 200.00]
Out[57]:
Mercado Global Data Vendas (em milhões) Variação de Vendas Variação Percentual
1 Africa 2016-09-30 0.49 0.40 444.44
7 Europe 2015-06-30 0.98 0.96 4800.00
13 Europe 2016-09-30 0.16 0.13 433.33
18 Europe 2017-06-30 0.43 0.32 290.91
34 Pacific Asia 2015-11-30 0.99 0.73 280.77
41 Pacific Asia 2016-09-30 0.29 0.21 262.50

As vendas por seguimento oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Europe - Junho/2015: 4800% - Junho e setembro foram meses bem favoráveis para o mercado Europeu.
  • Pacific Asia - Novembro/2015: 280,77% - Para este mercado, os meses novembro e setembro foram bem positivos.

Vendas por região do cliente¶

In [58]:
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_regiao_do_cliente = df_filtrado.groupby('Customer Country')['Sales'].sum().reset_index()

Vendas_por_regiao_do_cliente['Sales'] = (Vendas_por_regiao_do_cliente['Sales'] / 1000000).round(2)
Vendas_por_regiao_do_cliente.rename(columns={'Sales': 'Vendas (em milhões)', 'Customer Country': 'País de origem do cliente'}, inplace=True)

# Ordenando os resultados
Vendas_por_regiao_do_cliente = Vendas_por_regiao_do_cliente.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)

Vendas_por_regiao_do_cliente
Out[58]:
País de origem do cliente Vendas (em milhões)
0 EE. UU. 20.18
1 Puerto Rico 12.61
In [59]:
fig = px.bar(Vendas_por_regiao_do_cliente, x='País de origem do cliente', y='Vendas (em milhões)', color = 'País de origem do cliente',
             text = 'Vendas (em milhões)', title='Vendas por região de origem do cliente (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()

Os clientes da empresa são basicamente de duas nacionalidades: Estados Unidos e Porto Rico.

In [60]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_regiao_do_cliente_e_data = df_filtrado.groupby(['Customer Country', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Vendas_por_regiao_do_cliente_e_data['Sales'] = (Vendas_por_regiao_do_cliente_e_data['Sales'] / 1000000).round(2)

Vendas_por_regiao_do_cliente_e_data.rename(columns={'Customer Country':'País de origem do cliente', 'Sales': 'Vendas (em milhoes)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação das vendas por regiao do cliente
Vendas_por_regiao_do_cliente_e_data['Variação de Vendas'] = Vendas_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Vendas (em milhoes)'].diff()

# Calculando a variação percentual das vendas por regiao do cliente
Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] = Vendas_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Vendas (em milhoes)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] = (Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_regiao_do_cliente_e_data, x='Data', y='Vendas (em milhoes)', color='País de origem do cliente', title='Vendas por região do cliente ao Longo do Tempo (em milhoes)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [61]:
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_regiao_do_cliente_e_data, x='Data', y='Variação de Vendas', color='País de origem do cliente', title='Variação de vendas por região do cliente ao Longo do Tempo (em milhoes)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [62]:
# Destacando os mercados globais e os meses que tiveram uma variação maior que 10% nas vendas
Vendas_por_regiao_do_cliente_e_data[abs(Vendas_por_regiao_do_cliente_e_data['Variação Percentual']) > 10.00]
Out[62]:
País de origem do cliente Data Vendas (em milhoes) Variação de Vendas Variação Percentual
1 EE. UU. 2015-02-28 0.54 -0.09 -14.29
2 EE. UU. 2015-03-31 0.62 0.08 14.81
13 EE. UU. 2016-02-29 0.57 -0.08 -12.31
21 EE. UU. 2016-10-31 0.65 0.06 10.17
28 EE. UU. 2017-05-31 0.67 0.09 15.52
29 EE. UU. 2017-06-30 0.60 -0.07 -10.45
34 Puerto Rico 2015-02-28 0.34 -0.04 -10.53
35 Puerto Rico 2015-03-31 0.39 0.05 14.71
49 Puerto Rico 2016-05-31 0.40 0.06 17.65
59 Puerto Rico 2017-03-31 0.39 0.04 11.43
63 Puerto Rico 2017-07-31 0.42 0.04 10.53

Durante o período avaliado, percebe-se que:

  • Os Estados Unidos tiveram variações mais significativas:: Fevereiro/2015 (-14,29%), Março/2015 (14,81%) e Maio/2017 (15,52%)
  • Puerto Rico teve variações mais significativas: Maio/2015 (17,65%), Março/2015 (14,71%) e Março/2017 (11,43%)

Vendas por região de destino¶

In [63]:
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_destino = df_filtrado.groupby('Order Region')['Sales'].sum().reset_index()

Vendas_por_destino['Sales'] = (Vendas_por_destino['Sales'] / 1000000).round(2)

Vendas_por_destino.rename(columns={'Order Region':'Região de destino', 'Sales': 'Vendas (em milhões)'}, inplace=True)

# Ordenando os resultados
Vendas_por_destino = Vendas_por_destino.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)

Vendas_por_destino
Out[63]:
Região de destino Vendas (em milhões)
0 Central America 5.43
1 Western Europe 4.92
2 South America 2.81
3 Northern Europe 1.80
4 Southern Europe 1.71
5 Oceania 1.65
6 Caribbean 1.58
7 West of USA 1.49
8 Southeast Asia 1.47
9 East of USA 1.31
10 South Asia 1.25
11 Eastern Asia 1.12
12 West Asia 1.12
13 US Center 1.11
14 South of USA 0.75
15 Eastern Europe 0.75
16 West Africa 0.70
17 North Africa 0.61
18 East Africa 0.36
19 Central Africa 0.32
20 Southern Africa 0.22
21 Canada 0.18
22 Central Asia 0.11
In [64]:
fig = px.bar(Vendas_por_destino.head(10), x='Região de destino', y='Vendas (em milhões)', color = 'Região de destino',
             text = 'Vendas (em milhões)', title='Top 10 regiões de destino com mais vendas (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [65]:
# Obtendo a lista das regiões mais lucrativas no geral
top_5_regions = Vendas_por_destino.head(5)['Região de destino'].tolist()

# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_destino_e_data = df_filtrado.groupby(['Order Region', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()

Vendas_por_destino_e_data['Sales'] = (Vendas_por_destino_e_data['Sales'] / 1000000).round(2)

Vendas_por_destino_e_data.rename(columns={'Order Region':'Região de destino', 'Sales': 'Vendas (em milhões)', 'order date (DateOrders)':'Data'}, inplace=True)

# Calculando a variação das vendas por regiao de destino da entrega
Vendas_por_destino_e_data['Variação de Vendas'] = Vendas_por_destino_e_data.groupby('Região de destino')['Vendas (em milhões)'].diff()

# Calculando a variação percentual das vendas por regiao de destino da entrega
Vendas_por_destino_e_data['Variação Percentual'] = Vendas_por_destino_e_data.groupby('Região de destino')['Vendas (em milhões)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Vendas_por_destino_e_data['Variação Percentual'] = (Vendas_por_destino_e_data['Variação Percentual'] * 100).round(2)

# Filtrando do dataframe 'Lucro_por_regiao_e_data' somente as regiões do 'top_5_regions'
Vendas_por_destino_e_data_filtrado = Vendas_por_destino_e_data[Vendas_por_destino_e_data['Região de destino'].isin(top_5_regions)]

# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_destino_e_data_filtrado, x='Data', y='Vendas (em milhões)', color='Região de destino', title='Top 5 das regiões de destino com mais vendas ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [66]:
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_destino_e_data_filtrado, x='Data', y='Variação de Vendas', color='Região de destino', title='Variação das vendas das Top 5 regiões que mais venderam (em milhões)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [67]:
# Destacando os destinos das entregas e os meses que tiveram uma variação maior que 1000% nas vendas
Vendas_por_destino_e_data[~Vendas_por_destino_e_data['Variação Percentual'].isin([np.inf, -np.inf]) &
    (abs(Vendas_por_destino_e_data['Variação Percentual']) > 500.00)]
Out[67]:
Região de destino Data Vendas (em milhões) Variação de Vendas Variação Percentual
18 Central Africa 2016-09-30 0.07 0.06 600.0
41 East Africa 2016-09-30 0.08 0.07 700.0
63 Eastern Europe 2016-09-30 0.14 0.12 600.0
75 Northern Europe 2015-06-30 0.19 0.18 1800.0
86 Northern Europe 2017-06-30 0.08 0.07 700.0
150 Southern Europe 2017-06-30 0.07 0.06 600.0
160 West Africa 2016-09-30 0.15 0.13 650.0
177 Western Europe 2015-06-30 0.59 0.58 5800.0

As vendas de acordo com a região de destino oscilaram durante todo o período.

Destaque para os meses de variação positiva nas vendas:¶

  • Northern Europe - Junho/2015: 1800%

  • Western Europe - Junho/2015: 5800%

  • Regiões da Europa como um todo tiveram bons desempenhos nos meses de junho e setembro

6.3 Análise de Lucratividade¶

In [68]:
# Criando uma nova coluna, margem de lucro = (Benefício por pedido / Vendas) * 100
df_filtrado['Profit Margin'] = (df_filtrado['Benefit per order'] / df_filtrado['Sales']) * 100

# Visualizando a distribuição da margem de lucro
plt.figure(figsize=(16, 9))
sns.histplot(df_filtrado['Profit Margin'], bins=30, kde=True)
plt.title('Distribuição da Margem de Lucro')
plt.xlabel('Margem de Lucro (%)')
plt.ylabel('Frequência')
plt.show()
No description has been provided for this image
In [69]:
df_filtrado['Benefit per order'].describe()
Out[69]:
count    164572.000000
mean         21.449250
std          97.492779
min       -1844.979980
25%           7.420000
50%          31.670000
75%          63.860001
max         721.599976
Name: Benefit per order, dtype: float64
In [70]:
fig = sns.boxplot(y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90)
Out[70]:
([0], [Text(0, 0, '')])
No description has been provided for this image

O boxplot acima mostra que a mediana, Q1 (primeiro quartil) e Q3 (terceiro quartil) estão muito próximos a zero, ou seja, para grande maioria dos casos não há uma margem de lucro positiva significante na venda dos produtos, mesmo com os outliers acima do limite superior. Além disso, há outliers bem abaixo do limite inferior e bem abaixo de zero, o que indica que a distribuição é bem assimétrica à esquerda e também que grande parte das vendas gera prejuízo à empresa.

In [71]:
# Agrupar os dados por ano e mês e somar as vendas
benefit_over_time = df_filtrado.resample('M', on='order date (DateOrders)').sum()['Benefit per order']

# Criar um DataFrame a partir da série
benefit_over_time_df = benefit_over_time.reset_index()

# Calculando a variação das vendas
benefit_over_time_df['Variação do Lucro'] = benefit_over_time_df['Benefit per order'].diff()

# Calculando a variação percentual das vendas
benefit_over_time_df['Variação Percentual'] = benefit_over_time_df['Benefit per order'].pct_change()

# Convertendo a variação para porcentagem e arredondando
benefit_over_time_df['Variação Percentual'] = (benefit_over_time_df['Variação Percentual'] * 100).round(2)


# Visualização
fig = px.line(benefit_over_time_df, x='order date (DateOrders)', y='Benefit per order', 
              title='Lucros Totais por Mês', labels={'order date (DateOrders)': 'Data', 'Benefit per order': 'Lucros Totais'})
fig.show()
In [72]:
benefit_over_time.describe()
Out[72]:
count        33.000000
mean     106968.057397
std        8226.151101
min       83038.990604
25%      102710.810232
50%      106958.890036
75%      111232.069980
max      126668.110171
Name: Benefit per order, dtype: float64

Os lucros variam em torno de uma média de 107 mil, atingindo a máxima histórica com 126 mil, no mês de agosto de 2017.

In [73]:
# Visualização
fig = px.line(benefit_over_time_df, x='order date (DateOrders)', y='Variação do Lucro', 
              title='Variação dos Lucros Totais por Mês', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=500)

fig.show()
In [74]:
# Destacando os meses que tiveram uma variação maior que 10% nos lucros da empresa
benefit_over_time_df[abs(benefit_over_time_df['Variação Percentual']) > 10.00]
Out[74]:
order date (DateOrders) Benefit per order Variação do Lucro Variação Percentual
1 2015-02-28 93009.960174 -12763.610006 -12.07
2 2015-03-31 107401.920248 14391.960074 15.47
9 2015-10-31 96839.170095 -12760.050088 -11.64
13 2016-02-29 83038.990604 -19671.819628 -19.15
14 2016-03-31 97525.060235 14486.069631 17.44
15 2016-04-30 108641.619867 11116.559631 11.40
18 2016-07-31 114827.080170 12173.540082 11.86
20 2016-09-30 121734.689953 17168.649881 16.42
24 2017-01-31 113146.160262 14171.800296 14.32
31 2017-08-31 126668.110171 15436.040191 13.88

A lucratividade oscilou durante todo o período avaliado, mas com destaque para as variações:

  • Fevereiro/2016: -19,15% e Fevereiro/2015: -12,07%,
  • Março/2016: 16,42% e Março/2015: 15,47%

Lucratividade por categoria¶

In [75]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_categoria = df_filtrado.groupby('Category Name')['Benefit per order'].sum().reset_index()

Lucro_por_categoria['Benefit per order'] = (Lucro_por_categoria['Benefit per order'] / 1000).round(2)

Lucro_por_categoria.rename(columns={'Category Name':'Categoria','Benefit per order': 'Lucro (em milhares)'}, inplace=True)

# Ordenando os resultados
Lucro_por_categoria = Lucro_por_categoria.sort_values(by='Lucro (em milhares)', ascending=False).reset_index(drop=True)

Lucro_por_categoria
Out[75]:
Categoria Lucro (em milhares)
0 Fishing 730.16
1 Cleats 473.68
2 Camping & Hiking 409.22
3 Cardio Equipment 361.56
4 Women's Apparel 334.16
5 Water Sports 308.59
6 Indoor/Outdoor Games 302.19
7 Men's Footwear 296.36
8 Shop By Sport 124.81
9 Electronics 38.51
10 Girls' Apparel 16.92
11 Accessories 15.75
12 Golf Gloves 13.65
13 Baseball & Softball 12.24
14 Golf Shoes 11.60
15 Kids' Golf Clubs 9.51
16 Boxing & MMA 7.93
17 Golf Balls 7.57
18 Trade-In 7.36
19 Tennis & Racquet 5.97
20 Hockey 5.97
21 Hunting & Shooting 5.88
22 Women's Golf Clubs 5.37
23 Men's Golf Clubs 5.28
24 Fitness Accessories 5.06
25 Lacrosse 4.10
26 Soccer 3.85
27 Golf Apparel 3.40
28 Golf Bags & Carts 1.81
29 As Seen on TV! 1.45
30 Strength Training 0.48
31 Basketball -0.44
In [76]:
fig = px.bar(Lucro_por_categoria.head(10), x='Categoria', y='Lucro (em milhares)', color = 'Categoria',
             text = 'Lucro (em milhares)', title='Top 10 categorias mais lucrativas (em milhares)')

fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
In [77]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_categoria_e_data = df_filtrado.groupby(['Category Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_categoria_e_data.rename(columns={'Category Name':'Categoria', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação dos lucros por categoria
Lucro_por_categoria_e_data['Variação de Lucro'] = Lucro_por_categoria_e_data.groupby('Categoria')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por categoria
Lucro_por_categoria_e_data['Variação Percentual'] = Lucro_por_categoria_e_data.groupby('Categoria')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_categoria_e_data['Variação Percentual'] = (Lucro_por_categoria_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_categoria_e_data, x='Data', y='Lucro (em milhares)', color='Categoria', title='Lucro por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [78]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_categoria_e_data, x='Data', y='Variação de Lucro', color='Categoria', title='Variação no lucro por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [79]:
# Destacando os meses em que as categorias tiveram uma variação maior que 2000% nos lucros da empresa
Lucro_por_categoria_e_data[abs(Lucro_por_categoria_e_data['Variação Percentual']) > 2000.00]
Out[79]:
Categoria Data Lucro (em milhares) Variação de Lucro Variação Percentual
289 Fitness Accessories 2017-09-30 961.479996 954.089994 12910.55
352 Golf Bags & Carts 2017-05-31 723.549999 761.370005 -2013.14
478 Hockey 2017-09-30 1241.189989 1183.699989 2058.97
483 Hunting & Shooting 2015-05-31 -407.849995 -421.079991 -3182.77
657 Soccer 2017-05-31 1448.839995 1432.999994 9046.72
711 Trade-In 2016-04-30 432.400003 436.499995 -10646.36

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Fitness Accessories - Setembro/2017: 12910,55%
  • Soccer - Maio/2017: 6,41%
  • Hockey - Setembro/2017: 2058,97%

Destaque para os meses de variação negativa nos lucros:¶

  • Trade-In - Abril/2016: -10646,36%,
  • Hunting & Shooting - Maio/2015: -3182,77%
  • Golf Bags & Carts - Maio/2017: -2013,14%
In [80]:
sns.set(rc = {'figure.figsize':(23,11)})
fig = sns.boxplot(x='Category Name', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
No description has been provided for this image

Lucratividade por Mercado Global¶

In [81]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_market = df_filtrado.groupby('Market')['Benefit per order'].sum().reset_index()

Lucro_por_market['Benefit per order'] = (Lucro_por_market['Benefit per order'] / 1000000).round(2)

Lucro_por_market.rename(columns={'Benefit per order': 'Lucro (em milhões)'}, inplace=True)

# Ordenando os resultados
Lucro_por_market = Lucro_por_market.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)

Lucro_por_market
Out[81]:
Market Lucro (em milhões)
0 LATAM 1.07
1 Europe 1.00
2 Pacific Asia 0.68
3 USCA 0.54
4 Africa 0.24
In [82]:
fig = px.bar(Lucro_por_market, x='Market', y='Lucro (em milhões)', color = 'Market',
             text = 'Lucro (em milhões)', title='Lucro total por Mercado Global (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
In [83]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_market_e_data = df_filtrado.groupby(['Market', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_market_e_data.rename(columns={'Market':'Mercado Global', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação dos lucros por mercado global
Lucro_por_market_e_data['Variação de Lucro'] = Lucro_por_market_e_data.groupby('Mercado Global')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por mercado global
Lucro_por_market_e_data['Variação Percentual'] = Lucro_por_market_e_data.groupby('Mercado Global')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_market_e_data['Variação Percentual'] = (Lucro_por_market_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_market_e_data, x='Data', y='Lucro (em milhares)', color='Mercado Global', title='Lucro por Mercado Global ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [84]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_market_e_data, x='Data', y='Variação de Lucro', color='Mercado Global', title='Variação no lucro por Mercado Global ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [85]:
# Destacando os meses em que mercados globais tiveram uma variação maior que 1000% nos lucros da empresa
Lucro_por_market_e_data[abs(Lucro_por_market_e_data['Variação Percentual']) > 1000.00]
Out[85]:
Mercado Global Data Lucro (em milhares) Variação de Lucro Variação Percentual
7 Europe 2015-06-30 105576.890319 102701.120345 3571.26
13 Europe 2016-09-30 19998.460034 18804.170045 1574.51
41 Pacific Asia 2016-09-30 36197.930071 34461.110042 1984.15

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Europe - junho/2015: 3571,26%
  • Pacific Asia - setembro/2016: 1984,15%
In [86]:
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Market', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
No description has been provided for this image

Regiões de destino mais lucrativos para a empresa¶

In [87]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_destino = df_filtrado.groupby('Order Region')['Benefit per order'].sum().reset_index()

Lucro_por_destino['Benefit per order'] = (Lucro_por_destino['Benefit per order'] / 1000).round(2)

Lucro_por_destino.rename(columns={'Order Region':'Região de destino', 'Benefit per order': 'Lucro (em milhares)'}, inplace=True)

# Ordenando os resultados
Lucro_por_destino = Lucro_por_destino.sort_values(by='Lucro (em milhares)', ascending=False).reset_index(drop=True)

Lucro_por_destino
Out[87]:
Região de destino Lucro (em milhares)
0 Central America 586.57
1 Western Europe 531.38
2 South America 315.50
3 Northern Europe 197.23
4 Southern Europe 194.95
5 Caribbean 166.98
6 Oceania 166.39
7 West of USA 159.19
8 Southeast Asia 150.32
9 East of USA 148.77
10 South Asia 129.39
11 US Center 126.36
12 West Asia 115.46
13 Eastern Asia 107.08
14 South of USA 82.37
15 West Africa 77.12
16 Eastern Europe 76.85
17 North Africa 59.36
18 East Africa 41.89
19 Central Africa 32.43
20 Southern Africa 29.24
21 Canada 22.54
22 Central Asia 12.58
In [88]:
fig = px.bar(Lucro_por_destino.head(10), x='Região de destino', y='Lucro (em milhares)', color = 'Região de destino',
             text = 'Lucro (em milhares)', title='Top 10 regiões de destino mais lucrativas para a empresa (em milhares)')

fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [89]:
# Obtendo a lista das regiões mais lucrativas no geral
top_5_regions = Lucro_por_destino.head(5)['Região de destino'].tolist()

# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_destino_e_data = df_filtrado.groupby(['Order Region', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_destino_e_data.rename(columns={'Order Region':'Região de destino', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)':'Data'}, inplace=True)

# Calculando a variação dos lucros por destino
Lucro_por_destino_e_data['Variação de Lucro'] = Lucro_por_destino_e_data.groupby('Região de destino')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por destino
Lucro_por_destino_e_data['Variação Percentual'] = Lucro_por_destino_e_data.groupby('Região de destino')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_destino_e_data['Variação Percentual'] = (Lucro_por_destino_e_data['Variação Percentual'] * 100).round(2)

# Filtrando do dataframe 'Lucro_por_regiao_e_data' somente as regiões do 'top_5_regions'
Lucro_por_destino_e_data_filtrado = Lucro_por_destino_e_data[Lucro_por_destino_e_data['Região de destino'].isin(top_5_regions)]

# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_destino_e_data_filtrado, x='Data', y='Lucro (em milhares)', color='Região de destino', title='Lucro das 5 regiões de destino mais rentáveis ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [90]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_destino_e_data_filtrado, x='Data', y='Variação de Lucro', color='Região de destino', title='Variação do lucro das 5 regiões de destino mais rentáveis ao longo do tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [91]:
# Destacando os meses em que destinos tiveram uma variação maior que 2000% nos lucros da empresa
Lucro_por_destino_e_data[abs(Lucro_por_destino_e_data['Variação Percentual']) > 2000.00]
Out[91]:
Região de destino Data Lucro (em milhares) Variação de Lucro Variação Percentual
81 Northern Europe 2016-09-30 368.500006 383.520006 -2553.40
115 South Asia 2016-09-30 7493.699992 7542.719976 -15387.03
139 Southern Europe 2015-06-30 24983.670027 24487.640023 4936.73
150 Southern Europe 2017-06-30 7034.129999 7062.090028 -25257.81
177 Western Europe 2015-06-30 56169.680160 55215.900179 5789.17
188 Western Europe 2017-06-30 28751.510036 28514.820043 12047.33

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Western Europe - junho/2017: 12047,33%
  • Western Europe - junho/2015: 5789,17%
  • Southern Europe - Setembro/2017: 4936,73%

Destaque para os meses de variação negativa nos lucros:¶

  • Southern Europe - junho/2017: -25257,81%,
  • South Asia - setembro/2016: -15387,03%
  • Northern Europe - setembro/2016: -2553,40%
In [92]:
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Order Region', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
No description has been provided for this image

Segmentos de clientes mais lucrativos¶

In [93]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_segmento = df_filtrado.groupby('Customer Segment')['Benefit per order'].sum().reset_index()

Lucro_por_segmento['Benefit per order'] = (Lucro_por_segmento['Benefit per order'] / 1000000).round(2)

Lucro_por_segmento.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Customer Segment': 'Segmento de Cliente'}, inplace=True)

# Ordenando os resultados
Lucro_por_segmento = Lucro_por_segmento.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)

Lucro_por_segmento
Out[93]:
Segmento de Cliente Lucro (em milhões)
0 Consumer 1.83
1 Corporate 1.07
2 Home Office 0.63
In [94]:
fig = px.bar(Lucro_por_segmento, x='Segmento de Cliente', y='Lucro (em milhões)', color = 'Segmento de Cliente',
             text = 'Lucro (em milhões)', title='Lucratividade por segmento de cliente (em milhões)',
             labels={'Order Region': 'Região'})

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [95]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_segmento_e_data = df_filtrado.groupby(['Customer Segment', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_segmento_e_data.rename(columns={'Customer Segment':'Segmento', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação dos lucros por segmento
Lucro_por_segmento_e_data['Variação de Lucro'] = Lucro_por_segmento_e_data.groupby('Segmento')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por segmento
Lucro_por_segmento_e_data['Variação Percentual'] = Lucro_por_segmento_e_data.groupby('Segmento')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_segmento_e_data['Variação Percentual'] = (Lucro_por_segmento_e_data['Variação Percentual'] * 100).round(2)


# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_segmento_e_data, x='Data', y='Lucro (em milhares)', color='Segmento', title='Lucro por segmento de cliente ao Longo do Tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfic'
fig.show()
In [96]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_segmento_e_data, x='Data', y='Variação de Lucro', color='Segmento', title='Variação do lucro por segmento de cliente ao Longo do Tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfic'
fig.show()
In [97]:
# Destacando os meses em que segmentos tiveram uma variação maior que 40% nos lucros da empresa
Lucro_por_segmento_e_data[abs(Lucro_por_segmento_e_data['Variação Percentual']) > 40.00]
Out[97]:
Segmento Data Lucro (em milhares) Variação de Lucro Variação Percentual
44 Corporate 2015-12-31 33680.339986 10879.279855 47.71
85 Home Office 2016-08-31 12522.209888 -9716.130045 -43.69
97 Home Office 2017-08-31 25076.379961 9464.320037 60.62

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Home Office - agosto/2017: 60,62%
  • Corporate - dezembro/2015: 47,71%

Destaque para os meses de variação negativa nos lucros:¶

  • Home Office - agosto/2016: -43,69%
In [98]:
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Customer Segment', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
No description has been provided for this image

Departamentos mais lucrativos¶

In [99]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_depart = df_filtrado.groupby('Department Name')['Benefit per order'].sum().reset_index()

Lucro_por_depart['Benefit per order'] = (Lucro_por_depart['Benefit per order'] / 1000000).round(2)

Lucro_por_depart.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Department Name': 'Departamento'}, inplace=True)

# Ordenando os resultados
Lucro_por_depart = Lucro_por_depart.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)

Lucro_por_depart
Out[99]:
Departamento Lucro (em milhões)
0 Fan Shop 1.76
1 Apparel 0.77
2 Golf 0.48
3 Footwear 0.39
4 Outdoors 0.11
5 Fitness 0.03
In [100]:
fig = px.bar(Lucro_por_depart, x='Departamento', y='Lucro (em milhões)', color = 'Departamento',
             text = 'Lucro (em milhões)', title='Lucratividade por departamento (em milhões)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [101]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_depart_e_data = df_filtrado.groupby(['Department Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_depart_e_data.rename(columns={'Department Name':'Departamento', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação dos lucros por departamento
Lucro_por_depart_e_data['Variação de Lucro'] = Lucro_por_depart_e_data.groupby('Departamento')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por departamento
Lucro_por_depart_e_data['Variação Percentual'] = Lucro_por_depart_e_data.groupby('Departamento')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_depart_e_data['Variação Percentual'] = (Lucro_por_depart_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_depart_e_data, x='Data', y='Lucro (em milhares)', color='Departamento', title='Lucro por departamento ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [102]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_depart_e_data, x='Data', y='Variação de Lucro', color='Departamento', title='Variação do lucro por departamento ao longo do tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [103]:
# Destacando os meses em que departamentos tiveram uma variação maior que 300% nos lucros da empresa
Lucro_por_depart_e_data[abs(Lucro_por_depart_e_data['Variação Percentual']) > 300.00]
Out[103]:
Departamento Data Lucro (em milhares) Variação de Lucro Variação Percentual
80 Fitness 2016-03-31 841.719984 649.819970 338.62
97 Fitness 2017-08-31 1718.959974 1320.789978 331.72
113 Footwear 2016-03-31 10955.529980 8773.049867 401.98

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Footwear - março/2016: 401,98%
  • Fitness - março/2016: 338,62%
  • Fitness - agosto/2017: 331,72%

Lucro por região do cliente¶

In [104]:
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_regiao_do_cliente = df_filtrado.groupby('Customer Country')['Benefit per order'].sum().reset_index()

Lucro_por_regiao_do_cliente['Benefit per order'] = (Lucro_por_regiao_do_cliente['Benefit per order'] / 1000000).round(2)
Lucro_por_regiao_do_cliente.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Customer Country': 'País de origem do cliente'}, inplace=True)

# Ordenando os resultados
Lucro_por_regiao_do_cliente = Lucro_por_regiao_do_cliente.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)

Lucro_por_regiao_do_cliente
Out[104]:
País de origem do cliente Lucro (em milhões)
0 EE. UU. 2.18
1 Puerto Rico 1.35
In [105]:
fig = px.bar(Lucro_por_regiao_do_cliente, x='País de origem do cliente', y='Lucro (em milhões)', color = 'País de origem do cliente',
             text = 'Lucro (em milhões)', title='Lucro por região de origem do cliente')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [106]:
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_regiao_do_cliente_e_data = df_filtrado.groupby(['Customer Country', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()

Lucro_por_regiao_do_cliente_e_data['Benefit per order'] = (Lucro_por_regiao_do_cliente_e_data['Benefit per order'] / 1000).round(2)

Lucro_por_regiao_do_cliente_e_data.rename(columns={'Customer Country':'País de origem do cliente', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)

# Calculando a variação dos lucros por região do cliente
Lucro_por_regiao_do_cliente_e_data['Variação de Lucro'] = Lucro_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Lucro (em milhares)'].diff()

# Calculando a variação percentual dos lucros por região do cliente
Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] = Lucro_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Lucro (em milhares)'].pct_change()

# Convertendo a variação para porcentagem e arredondando
Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] = (Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] * 100).round(2)

# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_regiao_do_cliente_e_data, x='Data', y='Lucro (em milhares)', color='País de origem do cliente', title='Lucro por região do cliente - Ao Longo do Tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [107]:
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_regiao_do_cliente_e_data, x='Data', y='Variação de Lucro', color='País de origem do cliente', title='Variação do lucro por região do cliente - Ao Longo do Tempo')
fig.update_layout(width=1000, height=500)

# Mostrar o gráfico
fig.show()
In [108]:
# Destacando os meses em que departamentos tiveram uma variação maior que 30% nos lucros da empresa
Lucro_por_regiao_do_cliente_e_data[abs(Lucro_por_regiao_do_cliente_e_data['Variação Percentual']) > 30.00]
Out[108]:
País de origem do cliente Data Lucro (em milhares) Variação de Lucro Variação Percentual
45 Puerto Rico 2016-01-31 30.64 -15.26 -33.25
47 Puerto Rico 2016-03-31 39.55 11.04 38.72

Os lucros oscilaram durante todo o período.

Destaque para os meses de variação positiva nos lucros:¶

  • Puerto Rico - março/2016: 38,72%

Destaque para os meses de variação negativa nos lucros:¶

  • Puerto Rico - janeiro/2016: -33,25%

7. Análise de Operações de Envio

7.1 Risco de atraso nas entregas¶

In [109]:
risco_entrega_atrasada = df_filtrado['Late_delivery_risk'].apply(lambda lat: 'Risco Alto' if lat == 1 else 'Risco Baixo')
risco_entrega_atrasada_count = risco_entrega_atrasada.value_counts()

# Tamanho do gráfico
plt.figure(figsize=(8,6))

# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
    risco_entrega_atrasada_count.index, # valor no eixo x
    risco_entrega_atrasada_count.values, # valor no eixo y
    color = ['steelblue', 'lightcoral'] # cores das barras
)

# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de entregas', fontsize = 12)

# Titulo, letra tamanho 14
plt.title('Risco de atraso nas entregas da empresa', fontsize = 16)

# Adicionando a contagem em cima das barras
for bar in barra:
    yval = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)

plt.show()
No description has been provided for this image
In [110]:
risco_entrega_atrasada.value_counts(1).round(4)
Out[110]:
Late_delivery_risk
Risco Alto     0.5728
Risco Baixo    0.4272
Name: proportion, dtype: float64

Pela análise foi possível verificar que, no período entre janeiro/2015 à setembro/2017, 57.28% das entregas realizadas pela empresa tiveram um alto risco de atraso na entrega.

Risco de atraso por departamento¶

In [111]:
risco_atraso_depart = df_filtrado.groupby(['Department Name', 'Late_delivery_risk']).size().unstack(fill_value=0)

# Renomeando colunas 
risco_atraso_depart.rename(columns={'Department Name':'Departamento', 0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_depart = risco_atraso_depart.reset_index()

# Removendo a coluna Late_delivery_risk
risco_atraso_depart.columns = ['Departamento', 'Risco Baixo', 'Risco Alto']
In [112]:
risco_atraso_depart
Out[112]:
Departamento Risco Baixo Risco Alto
0 Apparel 19122 25588
1 Fan Shop 27155 36291
2 Fitness 856 1174
3 Footwear 5923 7934
4 Golf 13553 18179
5 Outdoors 3695 5102
In [113]:
fig = go.Figure()

# Risco de entrega baixo (Late_delivery_risk = 0)
fig.add_trace(go.Bar(x=risco_atraso_depart['Departamento'], y=risco_atraso_depart['Risco Baixo'], name='Risco Baixo', marker_color='blue'))

# Risco de entrega alto (Late_delivery_risk = 1)
fig.add_trace(go.Bar(x=risco_atraso_depart['Departamento'], y=risco_atraso_depart['Risco Alto'], name='Risco Alto', marker_color='red'))

# Atualizando o layout do gráfico
fig.update_layout(title='Risco de atraso em entrega por departamento', xaxis=dict(title='Nome do Departamento'), 
                  yaxis=dict(title='Contagem'), barmode='group')

fig.update_layout(width=1000, height=500)

# Mostrando o gráfico
fig.show()

Pelo gráfico acima pode-se concluir que para todos os departamentos o risco da entregas atrasar é majoritariamente alto.

Risco de atraso por mercado global¶

In [114]:
risco_atraso_mercado_global = df_filtrado.groupby(['Market', 'Late_delivery_risk']).size().unstack(fill_value=0)

# Renomeando colunas 
risco_atraso_mercado_global.rename(columns={0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_mercado_global = risco_atraso_mercado_global.reset_index()

# Removendo a coluna Late_delivery_risk
risco_atraso_mercado_global.columns = ['Mercado Global', 'Risco Baixo', 'Risco Alto']
In [115]:
risco_atraso_mercado_global
Out[115]:
Mercado Global Risco Baixo Risco Alto
0 Africa 4814 6340
1 Europe 19124 26153
2 LATAM 21265 28044
3 Pacific Asia 14612 19593
4 USCA 10489 14138
In [116]:
fig = go.Figure()

# Risco de entrega baixo (Late_delivery_risk = 0)
fig.add_trace(go.Bar(x=risco_atraso_mercado_global['Mercado Global'], y=risco_atraso_mercado_global['Risco Baixo'], name='Risco Baixo', marker_color='blue'))

# Risco de entrega alto (Late_delivery_risk = 1)
fig.add_trace(go.Bar(x=risco_atraso_mercado_global['Mercado Global'], y=risco_atraso_mercado_global['Risco Alto'], name='Risco Alto', marker_color='red'))

# Atualizando o layout do gráfico
fig.update_layout(title='Risco de atraso em entrega por Mercado Global', xaxis=dict(title='Mercado Global'), 
                  yaxis=dict(title='Contagem'), barmode='group')

fig.update_layout(width=1000, height=500)

# Mostrando o gráfico
fig.show()

Ou seja, em cada um dos mercados globais a maioria dos pedidos tem um alto risco da entrega atrasar.

Proporção de risco de atraso alto por categoria¶

In [117]:
risco_atraso_cat = df_filtrado.groupby(['Category Name', 'Late_delivery_risk']).size().unstack(fill_value=0)

# Renomeando colunas 
risco_atraso_cat.rename(columns={0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_cat = risco_atraso_cat.reset_index()

# Removendo a coluna Late_delivery_risk
risco_atraso_cat.columns = ['Category Name', 'Risco Baixo', 'Risco Alto']

# Criando uma coluna chamada 'Proporção de Alto Risco de Atraso'
risco_atraso_cat['Proporção de Alto Risco de Atraso'] = (risco_atraso_cat['Risco Alto']/(risco_atraso_cat['Risco Alto']+risco_atraso_cat['Risco Baixo'])).round(3)

# Ordenando o DataFrame pelo 'Proporção de Alto Risco de Atraso'
risco_atraso_cat.sort_values(by='Proporção de Alto Risco de Atraso', ascending=False, inplace=True)

risco_atraso_cat = risco_atraso_cat.reset_index(drop=True)

risco_atraso_cat
Out[117]:
Category Name Risco Baixo Risco Alto Proporção de Alto Risco de Atraso
0 Golf Bags & Carts 19 42 0.689
1 Lacrosse 123 206 0.626
2 Strength Training 40 60 0.600
3 Accessories 683 1014 0.598
4 Fitness Accessories 118 175 0.597
5 Boxing & MMA 164 238 0.592
6 As Seen on TV! 27 39 0.591
7 Trade-In 384 542 0.585
8 Electronics 1254 1770 0.585
9 Golf Gloves 427 602 0.585
10 Girls' Apparel 473 664 0.584
11 Tennis & Racquet 131 183 0.583
12 Hunting & Shooting 177 247 0.583
13 Golf Shoes 211 291 0.580
14 Shop By Sport 4453 6053 0.576
15 Baseball & Softball 258 349 0.575
16 Golf Balls 598 805 0.574
17 Cleats 9996 13481 0.574
18 Kids' Golf Clubs 150 202 0.574
19 Fishing 7072 9508 0.573
20 Water Sports 6347 8508 0.573
21 Indoor/Outdoor Games 7897 10548 0.572
22 Women's Apparel 8627 11462 0.571
23 Basketball 24 32 0.571
24 Women's Golf Clubs 73 97 0.571
25 Men's Footwear 9126 12107 0.570
26 Cardio Equipment 5131 6795 0.570
27 Camping & Hiking 5662 7480 0.569
28 Hockey 259 329 0.560
29 Soccer 61 75 0.551
30 Golf Apparel 200 229 0.534
31 Men's Golf Clubs 139 135 0.493

Pelo dataframe acima pode-se concluir que, com exceção de uma categoria (Men's Golf Clubs), em todos as outras os pedidos realizados têm alto risco de atraso na entrega em sua maioria.

7.2 Entrega real VS Entrega agendada¶

In [118]:
df_filtrado['Days for shipping (real)'].describe()
Out[118]:
count    164572.000000
mean          3.498232
std           1.623669
min           0.000000
25%           2.000000
50%           3.000000
75%           5.000000
max           6.000000
Name: Days for shipping (real), dtype: float64
In [119]:
df_filtrado['Days for shipping (real)'].value_counts()
Out[119]:
Days for shipping (real)
2    51648
3    26187
6    26186
4    25985
5    25715
0     4628
1     4223
Name: count, dtype: int64
In [120]:
df_filtrado['Days for shipment (scheduled)'].describe()
Out[120]:
count    164572.000000
mean          2.933051
std           1.373475
min           0.000000
25%           2.000000
50%           4.000000
75%           4.000000
max           4.000000
Name: Days for shipment (scheduled), dtype: float64
In [121]:
# Define a localidade para adicionar o separador de milhares
locale.setlocale(locale.LC_ALL, '')

# Cria uma figura com 2 subplots
fig, axs = plt.subplots(1, 2, figsize=(16, 9))

# Cria um boxplot para a variável 'entrega real' no primeiro subplot
bp1 = axs[0].boxplot(df_filtrado['Days for shipping (real)'], patch_artist=True)
axs[0].set_title('Boxplot dos dias reais de entrega')

# Calcula a média e mediana dias de entrega real
mean_entrega_real = np.mean(df_filtrado['Days for shipping (real)'])
median_entrega_real = np.median(df_filtrado['Days for shipping (real)'])

# Define a cor do boxplot
bp1['boxes'][0].set_facecolor('lightblue')

# Adiciona a legenda da média e mediana com separador de milhares
max_rent = np.max(df_filtrado['Days for shipping (real)'])
axs[0].annotate(f'Média = {locale.format_string("%.2f", mean_entrega_real, grouping=True)}\nMediana = {locale.format_string("%.2f", median_entrega_real, grouping=True)}', 
                xy=(1, max_rent*0.8), 
                xytext=(1.15, max_rent*0.8),
                bbox=dict(facecolor='lightblue', edgecolor='blue'),
                fontsize=10) 

# Cria um boxplot para a variável 'entrega agendada' no segundo subplot
bp2 = axs[1].boxplot(df_filtrado['Days for shipment (scheduled)'], patch_artist=True)
axs[1].set_title('Boxplot dos dias previstos de entrega')

# Calcula a média e mediana dos dias previstos para entrega
mean_entrega_agendada = np.mean(df_filtrado['Days for shipment (scheduled)'])
median_entrega_agendada = np.median(df_filtrado['Days for shipment (scheduled)'])

# Define a cor do boxplot
bp2['boxes'][0].set_facecolor('lightgreen')

# Adiciona a legenda da média e mediana com separador de milhares
max_total = np.max(df_filtrado['Days for shipment (scheduled)'])
axs[1].annotate(f'Média = {locale.format_string("%.2f", mean_entrega_agendada, grouping=True)}\nMediana = {locale.format_string("%.2f", median_entrega_agendada, grouping=True)}', 
                xy=(1, max_total*0.8), 
                xytext=(1.15, max_total*0.8),
                bbox=dict(facecolor='lightgreen', edgecolor='green'),
                fontsize=10)

# Mostra os gráficos
plt.show()
No description has been provided for this image

Pela análise dos boxplots:

Distribuição dos dias reais de entrega

  • O conjunto de dados dos dias reais de entrega não possui outliers superiores ou inferiores
  • O boxplot mostra que a mediana (Q2) está mais próxima de Q1 do que de Q3, mostrando que existe uma cauda mais longa com números maiores, possuindo mais registros com valores baixos do que altos, trata-se de uma distribuição assimétrica à direita. Isso indica que a maioria dos produtos demoram poucos dias para serem entregues (não significando que não estão em atraso).

Distribuição dos dias previstos de entrega

  • O conjunto de dados dos dias previstos de entrega não possui outliers superiores ou inferiores
  • O boxplot mostra que a mediana (Q2) é exatamente igual ao Q3 e também ao valor máximo, indicando que na grande maioria dos casos a empresa acaba prevendo uma mesma quantidade de dias para a entrega de produtos. Isso ainda contribui para a formação de uma cauda mais longa com números menores, possuindo mais registros com valores altos do que baixos, caracterizando uma distribuição assimétrica à esquerda
In [122]:
mediana_entrega_real = df_filtrado['Days for shipping (real)'].median()
mediana_entrega_agendada = df_filtrado['Days for shipment (scheduled)'].median()

mediana_entrega_real_format = (
    "{:,.0f}".format(mediana_entrega_real)
    .replace(",", ".")
)

mediana_entrega_agendada_format = (
    "{:,.0f}".format(mediana_entrega_agendada)
    .replace(",", ".")
)

# Crie dois subplots (um para o histograma de 'entrega real' e outro para o histograma de 'entrega agendada')
fig = make_subplots(rows=1, cols=2, subplot_titles=('Histograma dos dias para entrega - real', 'Histograma dos dias para entrega - previsto'))

# Adicione o histograma de 'entrega  real' ao primeiro subplot
histogram_entrega_real = go.Histogram(x=df_filtrado['Days for shipping (real)'], nbinsx=8, marker=dict(color='blue'))
fig.add_trace(histogram_entrega_real, row=1, col=1)

# Adicione o histograma de 'entrega agendada' ao segundo subplot
histogram_entrega_agendada = go.Histogram(x=df_filtrado['Days for shipment (scheduled)'], nbinsx=8, marker=dict(color='green'))
fig.add_trace(histogram_entrega_agendada, row=1, col=2)

# Adicione a linha da mediana a ambos os subplots
line_entrega_real = go.Scatter(x=[mediana_entrega_real, mediana_entrega_real], y=[0, 60000], mode='lines',
                              line=dict(color='lightblue', dash='dash'),
                              showlegend=True,
                              name=f"Mediana dos dias para entrega - real = {mediana_entrega_real_format}")
fig.add_trace(line_entrega_real, row=1, col=1)

line_entrega_agendada = go.Scatter(x=[mediana_entrega_agendada, mediana_entrega_agendada], y=[0, 120000], mode='lines',
                              line=dict(color='lightgreen', dash='dash'),
                              showlegend=True,
                              name=f"Mediana dos dias para entrega - agendada = {mediana_entrega_agendada_format}")
fig.add_trace(line_entrega_agendada, row=1, col=2)

# Atualize o layout e as configurações dos subplots
fig.update_layout(title_text='Histograma da quantidade de dias para entrega real e agendada',
                  autosize=False,
                  width=1200,  # Largura total dos subplots
                  height=500)

fig.update_yaxes(range=[0, 60000], row=1, col=1)
fig.update_yaxes(range=[0, 120000], row=1, col=2)

# Mostre o gráfico
fig.show()

Os histogramas indicados acima acabam comprovando o comportamento das distribuições verificado pelos boxplots anteriormente.

In [123]:
fig, axes = plt.subplots(1, 2, figsize = (18,9))
plt.tight_layout()

sns.boxplot(y=df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)'],ax= axes[0], palette = 'YlGnBu_r', orient='v')
axes[0].set_title('Boxplot - Delay entre dias reais e previstos para entrega', fontsize=18)


sns.histplot(df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)'], ax=axes[1], color='lightblue', bins=8)
axes[1].set_title('Histograma - Delay entre dias reais e previstos para entrega', fontsize=18)

plt.show()
No description has been provided for this image
In [124]:
delay = df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)']
delay.describe()
Out[124]:
count    164572.000000
mean          0.565181
std           1.492961
min          -2.000000
25%           0.000000
50%           1.000000
75%           1.000000
max           4.000000
dtype: float64

O boxplot e o histograma indicam que 75% dos pedidos possuem até 1 dia de atraso, sendo 4 dias o máximo de atraso que um pedido teve e 2 dias o máximo de adiantamento na chegada.

In [125]:
print(f"A assimetria da distribuição do delay assume valor: {skew(delay):.2f}, ou seja, é levemente assimétrica à esquerda.")
A assimetria da distribuição do delay assume valor: 0.03, ou seja, é levemente assimétrica à esquerda.
In [126]:
delay.value_counts(1)
Out[126]:
 1    0.335452
 0    0.186417
 2    0.159493
-2    0.120786
-1    0.119990
 3    0.039144
 4    0.038719
Name: proportion, dtype: float64
In [127]:
# Contando o número de pedidos com atraso de pelo menos 1 dia
delay_1_dia = (delay >= 1).sum()

# Calculando o número total de pedidos
total_de_pedidos = len(delay)

# Calculando a porcentagem de pedidos com atraso
porcentagem_com_atraso = (delay_1_dia / total_de_pedidos) * 100

# Exibindo o resultado
print(f"Porcentagem de pedidos com atraso de pelo menos 1 dia: {porcentagem_com_atraso:.2f}%")
print()
print(f"Porcentagem de pedidos com apenas 1 dia de atraso: {delay.value_counts(1)[1]:.2f}%")
print()
print(f"Porcentagem de pedidos com 2 dias de atraso: {delay.value_counts(1)[2]:.2f}%")
print()
print(f"Porcentagem de pedidos com 3 dias de atraso: {delay.value_counts(1)[3]:.2f}%")
print()
print(f"Porcentagem de pedidos com 4 dias de atraso: {delay.value_counts(1)[4]:.2f}%")
Porcentagem de pedidos com atraso de pelo menos 1 dia: 57.28%

Porcentagem de pedidos com apenas 1 dia de atraso: 0.34%

Porcentagem de pedidos com 2 dias de atraso: 0.16%

Porcentagem de pedidos com 3 dias de atraso: 0.04%

Porcentagem de pedidos com 4 dias de atraso: 0.04%
In [128]:
print(f"Porcentagem de pedidos com um dia adiantado: {delay.value_counts(1)[-1]:.2f}%")
print()
print(f"Porcentagem de pedidos com dois dias adiantado: {delay.value_counts(1)[-2]:.2f}%")
Porcentagem de pedidos com um dia adiantado: 0.12%

Porcentagem de pedidos com dois dias adiantado: 0.12%
In [129]:
print(f"Porcentagem de pedidos que chegou exatamente no dia previsto: {delay.value_counts(1)[0]:.2f}%")
Porcentagem de pedidos que chegou exatamente no dia previsto: 0.19%
In [130]:
# Obter a contagem para cada status de entrega

delay = (df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)']).value_counts()
delay_df = delay.reset_index()
delay_df.columns = ['Dias de Atraso', 'Quantidade de Pedidos']

# Convertendo 'Dias de Atraso' para string para obter cores distintas
delay_df['Dias de Atraso'] = delay_df['Dias de Atraso'].astype(str)


fig = px.bar(delay_df, y='Dias de Atraso', x='Quantidade de Pedidos', color='Dias de Atraso',
                 title="Quantidade de Pedidos de acordo com os dias de atraso", text = 'Quantidade de Pedidos')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()

7.3 Análise dos status de entrega¶

In [131]:
# Obter a contagem para cada status de entrega

status_entrega = df_filtrado['Delivery Status'].value_counts()
status_entrega_df = status_entrega.reset_index()
status_entrega_df.columns = ['Status de Entrega', 'Quantidade de Pedidos']


fig = px.bar(status_entrega_df, x='Status de Entrega', y='Quantidade de Pedidos', color='Status de Entrega',
                 title="Quantidade de Pedidos por Status de Entrega", text = 'Quantidade de Pedidos')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [132]:
status_entrega_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Delivery Status']).size()

status_entrega_tempo = status_entrega_tempo.unstack(fill_value=0)
status_entrega_tempo.index = status_entrega_tempo.index.to_timestamp()

fig = px.line(status_entrega_tempo, title='Status de Entrega ao Longo do Tempo',
                   labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Delivery Status': 'Status de Entrega'})

fig.update_layout(width=1000, height=500)
fig.show()
In [133]:
atraso_cat = df_filtrado.groupby(['Category Name', 'Delivery Status']).size().unstack(fill_value=0)
atraso_cat = atraso_cat.reset_index()

# Removendo a coluna Delivery Status
#atraso_cat.columns = ['Category Name', 'Advance shipping', 'Late delivery', 'Shipping canceled', 'Shipping on time']
atraso_cat.columns = ['Category Name', 'Advance shipping', 'Late delivery', 'Shipping on time']


# Criando uma coluna chamada 'Proporção de entregas em atraso'
atraso_cat['Proporção de entregas em atraso'] = (atraso_cat['Late delivery']/(atraso_cat['Late delivery']+atraso_cat['Advance shipping']+atraso_cat['Shipping on time'])).round(3)

# Ordenando o DataFrame pelo 'Proporção de entregas em atraso'
atraso_cat.sort_values(by='Proporção de entregas em atraso', ascending=False, inplace=True)

atraso_cat = atraso_cat.reset_index(drop=True)

atraso_cat
Out[133]:
Category Name Advance shipping Late delivery Shipping on time Proporção de entregas em atraso
0 Golf Bags & Carts 12 42 7 0.689
1 Lacrosse 75 206 48 0.626
2 Strength Training 20 60 20 0.600
3 Accessories 406 1014 277 0.598
4 Fitness Accessories 69 175 49 0.597
5 Boxing & MMA 84 238 80 0.592
6 As Seen on TV! 15 39 12 0.591
7 Trade-In 232 542 152 0.585
8 Electronics 733 1770 521 0.585
9 Golf Gloves 240 602 187 0.585
10 Girls' Apparel 284 664 189 0.584
11 Tennis & Racquet 65 183 66 0.583
12 Hunting & Shooting 101 247 76 0.583
13 Golf Shoes 120 291 91 0.580
14 Shop By Sport 2534 6053 1919 0.576
15 Baseball & Softball 141 349 117 0.575
16 Golf Balls 357 805 241 0.574
17 Cleats 5559 13481 4437 0.574
18 Kids' Golf Clubs 83 202 67 0.574
19 Fishing 4031 9508 3041 0.573
20 Water Sports 3539 8508 2808 0.573
21 Indoor/Outdoor Games 4444 10548 3453 0.572
22 Women's Apparel 4877 11462 3750 0.571
23 Basketball 20 32 4 0.571
24 Women's Golf Clubs 47 97 26 0.571
25 Men's Footwear 5095 12107 4031 0.570
26 Cardio Equipment 2915 6795 2216 0.570
27 Camping & Hiking 3142 7480 2520 0.569
28 Hockey 158 329 101 0.560
29 Soccer 28 75 33 0.551
30 Golf Apparel 116 229 84 0.534
31 Men's Golf Clubs 83 135 56 0.493

Pelo dataframe acima pode-se concluir que, com exceção de uma categoria (Men's Golf Clubs), em todos as outras a entregas em atraso superam a soma das entregas no prazo, canceladas e antes do prazo.

7.4 Análise dos status de pedido¶

In [134]:
# Obter a contagem para cada status de entrega

status_pedido = df_filtrado['Order Status'].value_counts()
status_pedido_df = status_pedido.reset_index()
status_pedido_df.columns = ['Status de Pedido', 'Quantidade de Pedidos']


fig = px.bar(status_pedido_df, x='Status de Pedido', y='Quantidade de Pedidos', color='Status de Pedido',
                 title="Quantidade de pedidos por status de pedido", text = 'Quantidade de Pedidos')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [135]:
status_pedido_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Order Status']).size()

status_pedido_tempo = status_pedido_tempo.unstack(fill_value=0)
status_pedido_tempo.index = status_pedido_tempo.index.to_timestamp()

fig = px.line(status_pedido_tempo, title='Status de Pedido ao Longo do Tempo',
                   labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Order Status': 'Status de Pedido'})

fig.update_layout(width=1000, height=500)
fig.show()

7.5 Análise de pedidos com entrega cancelada¶

In [136]:
entrega_cancelada_df = df[df['Delivery Status']=='Shipping canceled']
entrega_cancelada_df = entrega_cancelada_df['Order Status'].value_counts()

entrega_cancelada_df = entrega_cancelada_df.reset_index()
entrega_cancelada_df.columns = ['Motivo do Cancelamento', 'Quantidade de Pedidos']

entrega_cancelada_df['Motivo do Cancelamento'] = entrega_cancelada_df['Motivo do Cancelamento'].replace({'SUSPECTED_FRAUD': 'POSSIVEL FRAUDE', 'CANCELED': 'CANCELADO PELO USUARIO'})
entrega_cancelada_df
Out[136]:
Motivo do Cancelamento Quantidade de Pedidos
0 POSSIVEL FRAUDE 3877
1 CANCELADO PELO USUARIO 3513
In [137]:
fig = px.bar(entrega_cancelada_df, x='Motivo do Cancelamento', y='Quantidade de Pedidos', color='Motivo do Cancelamento',
                 title="Pedidos Com Entrega Cancelada", text = 'Quantidade de Pedidos')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Percebe-se que, dos pedidos que tiveram entrega cancelada, ou foram cancelados pelo próprio cliente ou por suspeita de fraude a própria empresa cancelou.

In [138]:
operacoes_fraude = df[df['Order Status']=='SUSPECTED_FRAUD']
operacoes_fraude.head()
Out[138]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode
183 TRANSFER 5 4 28.850000 128.220001 Shipping canceled 0 13 Electronics Freeport EE. UU. XXXXXXXXX Patricia 1509 Petersen XXXXXXXXX Consumer NY 3675 Emerald Goose Bank 11520.0 3 Footwear 40.654865 -73.587074 USCA Houston Estados Unidos 1509 2016-05-08 17:42:00 33824 278 6.75 0.05 84424 44.990002 0.23 3 134.970001 128.220001 28.850000 US Center Texas SUSPECTED_FRAUD 77041.0 278 13 NaN http://images.acmesports.sports/Under+Armour+Men%27s+Compression+EV+SL+Slide Under Armour Men's Compression EV SL Slide 44.990002 0 5/13/2016 17:42 Standard Class
184 TRANSFER 5 4 133.910004 278.970001 Shipping canceled 0 9 Cardio Equipment Fort Washington EE. UU. XXXXXXXXX Julie 1636 Petersen XXXXXXXXX Consumer MD 9375 Harvest Circuit 20744.0 3 Footwear 38.742664 -76.991798 USCA Gilbert Estados Unidos 1636 2016-04-02 19:51:00 31364 191 21.00 0.07 78398 99.989998 0.48 3 299.970001 278.970001 133.910004 West of USA Arizona SUSPECTED_FRAUD 85234.0 191 9 NaN http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe Nike Men's Free 5.0+ Running Shoe 99.989998 0 4/7/2016 19:51 Standard Class
185 TRANSFER 6 4 79.160004 272.970001 Shipping canceled 0 9 Cardio Equipment Bakersfield EE. UU. XXXXXXXXX Lisa 2784 Smith XXXXXXXXX Consumer CA 3993 Thunder Hills Port 93304.0 3 Footwear 35.362545 -119.018700 USCA San Jose Estados Unidos 2784 2016-08-14 02:51:00 40495 191 27.00 0.09 101052 99.989998 0.29 3 299.970001 272.970001 79.160004 West of USA California SUSPECTED_FRAUD 95123.0 191 9 NaN http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe Nike Men's Free 5.0+ Running Shoe 99.989998 0 8/20/2016 2:51 Standard Class
186 TRANSFER 5 4 19.110001 272.970001 Shipping canceled 0 9 Cardio Equipment Fort Washington EE. UU. XXXXXXXXX Julie 1636 Petersen XXXXXXXXX Consumer MD 9375 Harvest Circuit 20744.0 3 Footwear 38.742664 -76.991798 USCA Gilbert Estados Unidos 1636 2016-04-02 19:51:00 31364 191 27.00 0.09 78396 99.989998 0.07 3 299.970001 272.970001 19.110001 West of USA Arizona SUSPECTED_FRAUD 85234.0 191 9 NaN http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe Nike Men's Free 5.0+ Running Shoe 99.989998 0 4/7/2016 19:51 Standard Class
187 TRANSFER 2 4 10.770000 170.970001 Shipping canceled 0 17 Cleats Corona EE. UU. XXXXXXXXX Tyler 9174 Smith XXXXXXXXX Consumer NY 9414 Thunder Island Court 11368.0 4 Apparel 40.742107 -73.869621 USCA Fresno Estados Unidos 9174 2016-05-18 16:38:00 34506 365 9.00 0.05 86191 59.990002 0.06 3 179.970001 170.970001 10.770000 West of USA California SUSPECTED_FRAUD 93727.0 365 17 NaN http://images.acmesports.sports/Perfect+Fitness+Perfect+Rip+Deck Perfect Fitness Perfect Rip Deck 59.990002 0 5/20/2016 16:38 Standard Class

7.6 Análise de fraudes em operações¶

Fraudes em produtos¶

In [139]:
fraude_prod = operacoes_fraude.groupby('Product Name').size().reset_index(name='Compras Fraudulentas')
fraude_prod.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)

# Renomeando colunas 
fraude_prod.rename(columns={'Product Name': 'Nome do Produto'}, inplace=True)

fraude_prod
Out[139]:
Nome do Produto Compras Fraudulentas
51 Perfect Fitness Perfect Rip Deck 560
39 Nike Men's CJ Elite 2 TD Football Cleat 516
42 Nike Men's Dri-FIT Victory Golf Polo 481
49 O'Brien Men's Neoprene Life Vest 439
16 Field & Stream Sportsman 16 Gun Fire Safe 393
... ... ...
19 Garmin Forerunner 910XT GPS Watch 1
15 Elevation Training Mask 2.0 1
11 Diamondback Boys' Insight 24 Performance Hybr 1
5 Brooks Women's Ghost 6 Running Shoe 1
86 insta-bed Neverflat Air Mattress 1

87 rows × 2 columns

In [140]:
fig = px.bar(fraude_prod.head(10), x='Nome do Produto', y='Compras Fraudulentas', color='Nome do Produto',
                 title="Top 10 produtos com mais compras fraudulentas", text = 'Compras Fraudulentas')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Fraudes em categorias¶

In [141]:
fraude_cat = operacoes_fraude.groupby('Category Name').size().reset_index(name='Compras Fraudulentas')
fraude_cat.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)

# Renomeando colunas 
fraude_cat.rename(columns={'Category Name': 'Nome da Categoria'}, inplace=True)

fraude_cat
Out[141]:
Nome da Categoria Compras Fraudulentas
6 Cleats 560
20 Men's Footwear 516
28 Women's Apparel 481
17 Indoor/Outdoor Games 439
8 Fishing 393
27 Water Sports 329
4 Camping & Hiking 305
5 Cardio Equipment 276
22 Shop By Sport 228
7 Electronics 77
0 Accessories 44
10 Girls' Apparel 35
12 Golf Balls 33
26 Trade-In 27
13 Golf Gloves 22
18 Kids' Golf Clubs 13
15 Hockey 12
3 Boxing & MMA 12
1 Baseball & Softball 11
14 Golf Shoes 10
16 Hunting & Shooting 9
19 Lacrosse 8
29 Women's Golf Clubs 8
11 Golf Apparel 6
25 Tennis & Racquet 6
9 Fitness Accessories 6
21 Men's Golf Clubs 5
2 Basketball 3
23 Soccer 2
24 Strength Training 1
In [142]:
fig = px.bar(fraude_cat.head(10), x='Nome da Categoria', y='Compras Fraudulentas', color='Nome da Categoria',
                 title="Top 10 categorias com mais compras fraudulentas", text = 'Compras Fraudulentas')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Fraudes em departamentos¶

In [143]:
fraude_dep = operacoes_fraude.groupby('Department Name').size().reset_index(name='Compras Fraudulentas')
fraude_dep.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)

# Renomeando colunas 
fraude_dep.rename(columns={'Department Name': 'Departamento'}, inplace=True)

fraude_dep
Out[143]:
Departamento Compras Fraudulentas
1 Fan Shop 1475
0 Apparel 1076
4 Golf 744
3 Footwear 328
5 Outdoors 212
2 Fitness 42
In [144]:
fig = px.bar(fraude_dep.head(10), x='Departamento', y='Compras Fraudulentas', color='Departamento',
                 title="Compras fraudulentas por departamento", text = 'Compras Fraudulentas')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()

Fraudes por hora¶

In [145]:
# Criando a coluna target
df['target'] = df['Order Status'].apply(lambda x: 1 if x == 'SUSPECTED_FRAUD' else 0)

# Agrupando por hora e status de fraude e contando as ocorrências
fraude_por_hora = df.groupby([df['order date (DateOrders)'].dt.hour.rename('Hora'), 'target']).size().reset_index(name='Contagem')

# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_hora, x='Hora', y='Contagem', hue='target')

# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por hora do dia')
plt.xlabel('Hora do Dia')
plt.ylabel('Número de Transações')

# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])


plt.show()
No description has been provided for this image

Para verificar se há uma relação significativa entre a hora do dia e a ocorrência de fraudes, será realizado um teste de hipótese. Para tal, é escolhido o teste do Qui-Quadrado de Independência pois pretende-se verificar a independência entre duas variáveis categóricas em uma tabela de contingência. Esse teste compara as contagens observadas de cada combinação de categorias com as contagens que seriam esperadas se as variáveis fossem independentes. Se houver uma diferença significativa entre as contagens observadas e as esperadas, o teste indicará uma possível associação entre as variáveis.

H0: Não há associação entre as variáveis categóricas hora e fraude

HA: Há associação entre as variáveis categóricas hora e fraude

É definido um nível de significância (alpha) de 0.05 para o teste.

In [146]:
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.hour.rename('Hora'), df['target'])

# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)

print(f'O p-valor obtido foi de: {p:.6f}')

# Interpretação dos resultados
if p < 0.05:
    print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'Hora' e 'target'.")
else:
    print("Não rejeitamos a hipótese nula. 'Hora' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.000012
Rejeitamos a hipótese nula. Há uma relação significativa entre 'Hora' e 'target'.

Fraudes por dia¶

In [147]:
# Agrupando por dia e status de fraude e contando as ocorrências
fraude_por_dia = df.groupby([df['order date (DateOrders)'].dt.day.rename('Dia'), 'target']).size().reset_index(name='Contagem')

# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_dia, x='Dia', y='Contagem', hue='target')

# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por dia do mês')
plt.xlabel('Dia do Mês')
plt.ylabel('Número de Transações')

# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])


plt.show()
No description has been provided for this image

Para verificar se há uma relação significativa entre o dia do mês e a ocorrência de fraudes, será realizado um teste de hipótese:

H0: Não há associação entre as variáveis categóricas dia e fraude

HA: Há associação entre as variáveis categóricas dia e fraude

É definido um nível de significância (alpha) de 0.05 para o teste.

In [148]:
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.day.rename('Hora'), df['target'])

# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)

print(f'O p-valor obtido foi de: {p:.10f}')

# Interpretação dos resultados
if p < 0.05:
    print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'dia' e 'target'.")
else:
    print("Não rejeitamos a hipótese nula. 'dia' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.0000000082
Rejeitamos a hipótese nula. Há uma relação significativa entre 'dia' e 'target'.

Fraudes por mês¶

In [149]:
# Agrupando por mes e status de fraude e contando as ocorrências
fraude_por_mes = df.groupby([df['order date (DateOrders)'].dt.month.rename('Mes'), 'target']).size().reset_index(name='Contagem')

# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_mes, x='Mes', y='Contagem', hue='target')

# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por meses do ano')
plt.xlabel('Mês')
plt.ylabel('Número de Transações')

# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])


plt.show()
No description has been provided for this image

Para verificar se há uma relação significativa entre o mês do ano e a ocorrência de fraudes, será realizado um teste de hipótese:

H0: Não há associação entre as variáveis categóricas mês e fraude

HA: Há associação entre as variáveis categóricas mês e fraude

É definido um nível de significância (alpha) de 0.05 para o teste.

In [150]:
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.month.rename('Mes'), df['target'])

# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)

print(f'O p-valor obtido foi de: {p:.10f}')

# Interpretação dos resultados
if p < 0.05:
    print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'mes' e 'target'.")
else:
    print("Não rejeitamos a hipótese nula. 'mes' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.0000735397
Rejeitamos a hipótese nula. Há uma relação significativa entre 'mes' e 'target'.

7.7 Análise dos modos de entrega¶

In [151]:
# Obter a contagem para cada status de entrega

modos_entrega = df_filtrado['Shipping Mode'].value_counts()
modos_entrega_df = modos_entrega.reset_index()
modos_entrega_df.columns = ['Modo de Entrega', 'Quantidade de Pedidos']


fig = px.bar(modos_entrega_df, x='Modo de Entrega', y='Quantidade de Pedidos', color='Modo de Entrega',
                 title="Quantidade de pedidos por modo de envio", text = 'Quantidade de Pedidos')

fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()
In [152]:
modos_entrega_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Shipping Mode']).size()

modos_entrega_tempo = modos_entrega_tempo.unstack(fill_value=0)
modos_entrega_tempo.index = modos_entrega_tempo.index.to_timestamp()

fig = px.line(modos_entrega_tempo, title='Modos de Entrega ao Longo do Tempo',
                   labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Shipping Mode': 'Modo de Entrega'})
fig.update_layout(width=1000, height=500)

fig.show()
In [153]:
# Criando a coluna que mostra se a entrega foi atrasada (1) ou não (0)
df_filtrado['Atraso'] = (df_filtrado['Days for shipping (real)'] > df_filtrado['Days for shipment (scheduled)']).astype(int)

# Agrupando por 'shipping mode' e contando atrasos
atraso_por_modo_de_entrega = df_filtrado.groupby('Shipping Mode')['Atraso'].value_counts().unstack(fill_value=0)

# Calculando a porcentagem de atrasos
atraso_por_modo_de_entrega['Entregas Totais'] = atraso_por_modo_de_entrega.sum(axis=1)
atraso_por_modo_de_entrega['Porcentagem de Atraso'] = ((atraso_por_modo_de_entrega[1] / atraso_por_modo_de_entrega['Entregas Totais']) * 100).round(2)

# Criando o dataframe final
final_df = atraso_por_modo_de_entrega.rename(columns={1: 'Entregas Atrasadas', 0: 'Entregas à Tempo'})
final_df = final_df[['Entregas Totais', 'Entregas Atrasadas', 'Porcentagem de Atraso']].reset_index()

#final_df = final_df.set_index('Atraso')
#final_df.reset_index(drop=True, inplace=True)
In [154]:
final_df
Out[154]:
Atraso Shipping Mode Entregas Totais Entregas Atrasadas Porcentagem de Atraso
0 First Class 25270 25270 100.00
1 Same Day 8851 4223 47.71
2 Second Class 32188 25688 79.81
3 Standard Class 98263 39087 39.78
In [155]:
fig = px.bar(final_df, x='Shipping Mode', y='Porcentagem de Atraso', color='Shipping Mode',
                 title="Porcentagem de atraso por modo de envio", text = 'Porcentagem de Atraso',
            labels={'Shipping Mode': 'Modo de Envio'})

fig.update_traces(textposition='outside', texttemplate='%{text}%', textfont_size=12)
fig.update_layout(width=1000, height=500)

fig.show()

Pelo gráfico acima, percebe-se que ocorreram com atraso:

  • Todas (100%) as entregas do modo First Class.
  • Quase metade (47,71%) das entregas do modo Same Day.
  • Praticamente 80% (79,81%) das entregas do modo Second Class.
  • Quase 40% (39,78%) das entregas do modo Standard Class.

8. Análise dos tipos de pagamento

In [156]:
tipo_pagamento = df_filtrado['Type'].value_counts()

# Tamanho do gráfico
plt.figure(figsize=(8,6))

# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
    tipo_pagamento.index, # valor no eixo x
    tipo_pagamento.values, # valor no eixo y
    color = ['steelblue', 'lightcoral', 'olive','#ff7f50'] # cores das barras
)

# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de pedidos', fontsize = 12)

# Titulo, letra tamanho 14
plt.title('Quantidade de pedidos por método de pagamento', fontsize = 16)

# Adicionando a contagem em cima das barras
for bar in barra:
    yval = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)

plt.show()
No description has been provided for this image
In [157]:
# Porcentagem de pedidos pagos para cada modo de pagamento
df_filtrado['Type'].value_counts(1)
Out[157]:
Type
DEBIT       0.401107
TRANSFER    0.243814
PAYMENT     0.241797
CASH        0.113282
Name: proportion, dtype: float64
In [158]:
tipo_pagamento_mensal = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Type']).size()

tipo_pagamento_mensal = tipo_pagamento_mensal.unstack(fill_value=0)
tipo_pagamento_mensal.index = tipo_pagamento_mensal.index.to_timestamp()

fig = px.line(tipo_pagamento_mensal, title='Tipos de pagamento ao longo do tempo',
                   labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Type': 'Tipo de Pagamento'})

fig.update_layout(width=1000, height=500)

fig.show()

9. Análise dos preços e descontos

In [159]:
mediana_precos = df_filtrado['Order Item Product Price'].median()
mediana_descontos = df_filtrado['Order Item Discount'].median()

mediana_precos_format = (
    "{:,.0f}".format(mediana_precos)
    .replace(",", ".")
)

mediana_descontos_format = (
    "{:,.0f}".format(mediana_descontos)
    .replace(",", ".")
)

# Crie dois subplots (um para o histograma de 'preços' e outro para o histograma de 'descontos')
fig = make_subplots(rows=1, cols=2, subplot_titles=('Histograma dos preços', 'Histograma dos descontos'))

# Adicione o histograma de 'preços' ao primeiro subplot
histogram_preços = go.Histogram(x=df_filtrado['Order Item Product Price'], nbinsx=10, marker=dict(color='blue'))
fig.add_trace(histogram_preços, row=1, col=1)

# Adicione o histograma de 'descontos' ao segundo subplot
histogram_descontos = go.Histogram(x=df_filtrado['Order Item Discount'], nbinsx=12, marker=dict(color='green'))
fig.add_trace(histogram_descontos, row=1, col=2)

# Adicione a linha da mediana a ambos os subplots
line_precos = go.Scatter(x=[mediana_precos, mediana_precos], y=[0, 144844], mode='lines',
                              line=dict(color='lightblue', dash='dash'),
                              showlegend=True,
                              name=f"Mediana dos preços = {mediana_precos_format}")
fig.add_trace(line_precos, row=1, col=1)

line_descontos = go.Scatter(x=[mediana_descontos, mediana_descontos], y=[0, 125593], mode='lines',
                              line=dict(color='lightgreen', dash='dash'),
                              showlegend=True,
                              name=f"Mediana dos descontos = {mediana_descontos_format}")
fig.add_trace(line_descontos, row=1, col=2)

# Atualize o layout e as configurações dos subplots
fig.update_layout(title_text='Histograma dos preços e descontos dos produtos',
                  autosize=False,
                  width=1200,  # Largura total dos subplots
                  height=500)

fig.update_yaxes(range=[0, 150000], row=1, col=1)
fig.update_yaxes(range=[0, 150000], row=1, col=2)

# Mostre o gráfico
fig.show()
In [160]:
# Define a localidade para adicionar o separador de milhares
locale.setlocale(locale.LC_ALL, '')

# Cria uma figura com 2 subplots
fig, axs = plt.subplots(1, 2, figsize=(16, 9))

# Cria um boxplot para a variável 'preço' no primeiro subplot
bp1 = axs[0].boxplot(df_filtrado['Order Item Product Price'], patch_artist=True)
axs[0].set_title('Boxplot dos preços')

# Calcula a média e mediana do preço
mean_preco = np.mean(df_filtrado['Order Item Product Price'])
median_preco = np.median(df_filtrado['Order Item Product Price'])

# Define a cor do boxplot
bp1['boxes'][0].set_facecolor('lightblue')

# Adiciona a legenda da média e mediana com separador de milhares
max_rent = np.max(df_filtrado['Order Item Product Price'])
axs[0].annotate(f'Média = {locale.format_string("%.2f", mean_preco, grouping=True)}\nMediana = {locale.format_string("%.2f", median_preco, grouping=True)}', 
                xy=(1, max_rent*0.8), 
                xytext=(1.15, max_rent*0.8),
                bbox=dict(facecolor='lightblue', edgecolor='blue'),
                fontsize=10) 

# Cria um boxplot para a variável 'desconto' no segundo subplot
bp2 = axs[1].boxplot(df_filtrado['Order Item Discount'], patch_artist=True)
axs[1].set_title('Boxplot dos descontos')

# Calcula a média e mediana do desconto
mean_desconto = np.mean(df_filtrado['Order Item Discount'])
median_desconto = np.median(df_filtrado['Order Item Discount'])

# Define a cor do boxplot
bp2['boxes'][0].set_facecolor('lightgreen')

# Adiciona a legenda da média e mediana com separador de milhares
max_total = np.max(df_filtrado['Order Item Discount'])
axs[1].annotate(f'Média = {locale.format_string("%.2f", mean_desconto, grouping=True)}\nMediana = {locale.format_string("%.2f", median_desconto, grouping=True)}', 
                xy=(1, max_total*0.8), 
                xytext=(1.15, max_total*0.8),
                bbox=dict(facecolor='lightgreen', edgecolor='green'),
                fontsize=10)

# Mostra os gráficos
plt.show()
No description has been provided for this image
In [161]:
df_filtrado['Order Item Product Price'].describe()
Out[161]:
count    164572.000000
mean        133.657817
std         117.673170
min           9.990000
25%          50.000000
50%          59.990002
75%         199.990005
max        1999.989990
Name: Order Item Product Price, dtype: float64
In [162]:
df_filtrado['Order Item Discount'].describe()
Out[162]:
count    164572.000000
mean         20.201156
std          19.792135
min           0.000000
25%           5.500000
50%          14.000000
75%          29.990000
max         500.000000
Name: Order Item Discount, dtype: float64
In [163]:
# Assimetria

print(f"A assimetria da distribuição dos preços assume valor: {skew(df_filtrado['Order Item Product Price']):.2f}")
print(f"A assimetria da distribuição dos descontos assume valor: {skew(df_filtrado['Order Item Discount']):.2f}")
A assimetria da distribuição dos preços assume valor: 1.37
A assimetria da distribuição dos descontos assume valor: 1.74

Distribuição de preços¶

  • A mediana Q2 está muito mais próxima de Q1 do que de Q3, indicando que a maioria dos preços assume um valor pequeno e que a distribuição possui assimetria à direita (calculada em 1.37), o que é acentuado pelos outliers superiores.

Distribuição de descontos¶

  • A mediana Q2 está mais próxima de Q1 do que de Q3, indicando que a maioria dos descontos assume um valor pequeno e que a distribuição também possui assimetria à direita (calculada em 1.74), o que é acentuado pelos outliers superiores. Neste caso, os dados estão mais centralizados (média mais próxima da mediana) e menos dispersos (desvio padrão menor), em comparação à distribuição dos preços.

10. Análise de produtos mais vendidos e lucrativos

10.1 Análise dos produtos mais vendidos¶

In [164]:
# Calcular o valor total de vendas por item

nome_produto = df_filtrado.groupby('Product Name', as_index=False)['Sales'].sum()

nome_produto = nome_produto.rename(columns={'Sales': 'Vendas Totais', 'Product Name':'Nome do Produto'})


# Calcular o valor total de vendas de todos os itens
valor_total_vendas = df_filtrado['Sales'].sum()

# Calcular a porcentagem do valor total de vendas para cada item
nome_produto['Percentual_do_valor_de_venda'] = ((nome_produto['Vendas Totais'] / valor_total_vendas) * 100).round(2)

# Ordenar os itens por contribuição de valor descendente
nome_produto_ordenado = nome_produto.sort_values('Percentual_do_valor_de_venda', ascending=False)

# Calcular a porcentagem acumulada do valor total
nome_produto_ordenado['Percentual_acumulado'] = nome_produto_ordenado['Percentual_do_valor_de_venda'].cumsum()

nome_produto_ordenado.head(20)
Out[164]:
Nome do Produto Vendas Totais Percentual_do_valor_de_venda Percentual_acumulado
18 Field & Stream Sportsman 16 Gun Fire Safe 6.631669e+06 20.23 20.23
60 Perfect Fitness Perfect Rip Deck 4.226596e+06 12.89 33.12
15 Diamondback Women's Serene Classic Comfort Bi 3.942337e+06 12.02 45.14
50 Nike Men's Free 5.0+ Running Shoe 3.501150e+06 10.68 55.82
48 Nike Men's Dri-FIT Victory Golf Polo 3.007700e+06 9.17 64.99
59 Pelican Sunstream 100 Kayak 2.962852e+06 9.04 74.03
56 O'Brien Men's Neoprene Life Vest 2.762145e+06 8.42 82.45
45 Nike Men's CJ Elite 2 TD Football Cleat 2.760078e+06 8.42 90.87
85 Under Armour Girls' Toddler Spine Surge Runni 1.213896e+06 3.70 94.57
98 adidas Youth Germany Black/Red Away Match Soc 6.349000e+04 0.19 94.76
35 LIJA Women's Eyelet Sleeveless Golf Polo 5.869500e+04 0.18 94.94
96 adidas Men's F10 Messi TRX FG Soccer Cleat 5.471088e+04 0.17 95.11
81 Titleist Pro V1x High Numbers Personalized Go 4.554324e+04 0.14 95.25
78 Titleist Pro V1 High Numbers Personalized Gol 4.263180e+04 0.13 95.38
46 Nike Men's Comfort 2 Slide 4.292046e+04 0.13 95.51
92 Under Armour Women's Micro G Skulpt Running S 4.419588e+04 0.13 95.64
79 Titleist Pro V1x Golf Balls 4.074351e+04 0.12 95.76
80 Titleist Pro V1x High Numbers Golf Balls 3.728823e+04 0.11 95.87
88 Under Armour Men's Compression EV SL Slide 3.486725e+04 0.11 95.98
11 Clicgear Rovic Cooler Bag 3.307173e+04 0.10 96.08
In [165]:
# Categorizando os produtos

nome_produto_ordenado['Categoria'] = 'C'
nome_produto_ordenado.loc[nome_produto_ordenado['Percentual_acumulado'] <= 80, 'Categoria'] = 'A'
nome_produto_ordenado.loc[(nome_produto_ordenado['Percentual_acumulado'] > 80) & (nome_produto_ordenado['Percentual_acumulado'] <= 95), 'Categoria'] = 'B'

nome_produto_ordenado['Vendas Totais (em milhoes)'] = (nome_produto_ordenado['Vendas Totais']/1000000).round(3) 
nome_produto_ordenado = nome_produto_ordenado.drop('Vendas Totais', axis=1)

nome_produto_ordenado.head(20)
Out[165]:
Nome do Produto Percentual_do_valor_de_venda Percentual_acumulado Categoria Vendas Totais (em milhoes)
18 Field & Stream Sportsman 16 Gun Fire Safe 20.23 20.23 A 6.632
60 Perfect Fitness Perfect Rip Deck 12.89 33.12 A 4.227
15 Diamondback Women's Serene Classic Comfort Bi 12.02 45.14 A 3.942
50 Nike Men's Free 5.0+ Running Shoe 10.68 55.82 A 3.501
48 Nike Men's Dri-FIT Victory Golf Polo 9.17 64.99 A 3.008
59 Pelican Sunstream 100 Kayak 9.04 74.03 A 2.963
56 O'Brien Men's Neoprene Life Vest 8.42 82.45 B 2.762
45 Nike Men's CJ Elite 2 TD Football Cleat 8.42 90.87 B 2.760
85 Under Armour Girls' Toddler Spine Surge Runni 3.70 94.57 B 1.214
98 adidas Youth Germany Black/Red Away Match Soc 0.19 94.76 B 0.063
35 LIJA Women's Eyelet Sleeveless Golf Polo 0.18 94.94 B 0.059
96 adidas Men's F10 Messi TRX FG Soccer Cleat 0.17 95.11 C 0.055
81 Titleist Pro V1x High Numbers Personalized Go 0.14 95.25 C 0.046
78 Titleist Pro V1 High Numbers Personalized Gol 0.13 95.38 C 0.043
46 Nike Men's Comfort 2 Slide 0.13 95.51 C 0.043
92 Under Armour Women's Micro G Skulpt Running S 0.13 95.64 C 0.044
79 Titleist Pro V1x Golf Balls 0.12 95.76 C 0.041
80 Titleist Pro V1x High Numbers Golf Balls 0.11 95.87 C 0.037
88 Under Armour Men's Compression EV SL Slide 0.11 95.98 C 0.035
11 Clicgear Rovic Cooler Bag 0.10 96.08 C 0.033
In [166]:
fig = px.bar(nome_produto_ordenado[nome_produto_ordenado['Categoria'] == 'A'], x='Nome do Produto', y='Vendas Totais (em milhoes)', color='Nome do Produto',
                 title="Produtos que correspondem a 80% do valor total de vendas", text = 'Vendas Totais (em milhoes)')

fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Os produtos do gráfico acima correspondem a 80% das vendas (em valores financeiros) da DataCo Global:

  • Field & Stream Sportsman 16 Gun Fire Safe
  • Perfect Fitness Perfect Rip Deck
  • Diamondback Women's Serene Classic Comfort Bi
  • Nike Men's Free 5.0+ Running Shoe
  • Nike Men's Dri-FIT Victory Golf Polo
  • Pelican Sunstream 100 Kayak
  • Nike Men's CJ Elite 2 TD Football Cleat
In [167]:
# Juntando todos os nomes de produtos em uma única string
produtos_venda = " ".join(nome_produto for nome_produto in df['Product Name'])

# gerando a nuvem de palavras
wordcloud = WordCloud(background_color="white").generate(produtos_venda)

# plotando a nuvem de palavras
plt.figure(figsize=(10, 5))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
No description has been provided for this image

Com base nas informações encontradas, a DataCo Global poderia adotar as seguintes ações:

Campanhas Direcionadas: Concentrar esforços de marketing e vendas nos produtos mais lucrativos, utilizando campanhas direcionadas para impulsionar ainda mais as vendas desses produtos.

Pacotes e Promoções: Criar pacotes de produtos ou promoções que incluam produtos populares junto com itens menos vendidos para aumentar a exposição destes últimos.

10.2 Análise dos produtos mais lucrativos¶

In [168]:
# Calcular o valor total de lucro por item

nome_produto_lucro = df_filtrado.groupby('Product Name', as_index=False)['Order Profit Per Order'].sum()

nome_produto_lucro = nome_produto_lucro.rename(columns={'Order Profit Per Order': 'Lucros Totais', 'Product Name':'Nome do Produto'})


# Calcular o valor total de lucros de todos os itens
valor_total_lucros = df_filtrado['Order Profit Per Order'].sum()

# Calcular a porcentagem do valor total de lucros para cada item
nome_produto_lucro['Percentual_do_valor_de_lucro'] = ((nome_produto_lucro['Lucros Totais'] / valor_total_lucros) * 100).round(2)

# Ordenar os itens por contribuição de valor descendente
nome_produto_lucro_ordenado = nome_produto_lucro.sort_values('Percentual_do_valor_de_lucro', ascending=False)

# Calcular a porcentagem acumulada do valor total
nome_produto_lucro_ordenado['Percentual_acumulado'] = nome_produto_lucro_ordenado['Percentual_do_valor_de_lucro'].cumsum()

nome_produto_lucro_ordenado.head(20)
Out[168]:
Nome do Produto Lucros Totais Percentual_do_valor_de_lucro Percentual_acumulado
18 Field & Stream Sportsman 16 Gun Fire Safe 730159.127155 20.68 20.68
60 Perfect Fitness Perfect Rip Deck 472867.789776 13.40 34.08
15 Diamondback Women's Serene Classic Comfort Bi 409215.238132 11.59 45.67
50 Nike Men's Free 5.0+ Running Shoe 358365.128570 10.15 55.82
48 Nike Men's Dri-FIT Victory Golf Polo 334162.569531 9.47 65.29
59 Pelican Sunstream 100 Kayak 307522.800055 8.71 74.00
56 O'Brien Men's Neoprene Life Vest 302190.730486 8.56 82.56
45 Nike Men's CJ Elite 2 TD Football Cleat 296357.120152 8.40 90.96
85 Under Armour Girls' Toddler Spine Surge Runni 121568.960312 3.44 94.40
96 adidas Men's F10 Messi TRX FG Soccer Cleat 8078.230037 0.23 94.63
98 adidas Youth Germany Black/Red Away Match Soc 7467.999986 0.21 94.84
81 Titleist Pro V1x High Numbers Personalized Go 6225.599967 0.18 95.02
35 LIJA Women's Eyelet Sleeveless Golf Polo 6391.460083 0.18 95.20
46 Nike Men's Comfort 2 Slide 5969.959987 0.17 95.37
78 Titleist Pro V1 High Numbers Personalized Gol 6003.729949 0.17 95.54
80 Titleist Pro V1x High Numbers Golf Balls 4375.630023 0.12 95.66
66 TYR Boys' Team Digi Jammer 4118.670000 0.12 95.78
86 Under Armour Hustle Storm Medium Duffle Bag 4096.680019 0.12 95.90
92 Under Armour Women's Micro G Skulpt Running S 4084.070001 0.12 96.02
79 Titleist Pro V1x Golf Balls 3493.839973 0.10 96.12
In [169]:
# Categorizando os produtos

nome_produto_lucro_ordenado['Categoria'] = 'C'
nome_produto_lucro_ordenado.loc[nome_produto_lucro_ordenado['Percentual_acumulado'] <= 80, 'Categoria'] = 'A'
nome_produto_lucro_ordenado.loc[(nome_produto_lucro_ordenado['Percentual_acumulado'] > 80) & (nome_produto_lucro_ordenado['Percentual_acumulado'] <= 95), 'Categoria'] = 'B'

nome_produto_lucro_ordenado['Lucros Totais (em milhares)'] = (nome_produto_lucro_ordenado['Lucros Totais']/1000).round(3) 
nome_produto_lucro_ordenado = nome_produto_lucro_ordenado.drop('Lucros Totais', axis=1)

nome_produto_lucro_ordenado.head(20)
Out[169]:
Nome do Produto Percentual_do_valor_de_lucro Percentual_acumulado Categoria Lucros Totais (em milhares)
18 Field & Stream Sportsman 16 Gun Fire Safe 20.68 20.68 A 730.159
60 Perfect Fitness Perfect Rip Deck 13.40 34.08 A 472.868
15 Diamondback Women's Serene Classic Comfort Bi 11.59 45.67 A 409.215
50 Nike Men's Free 5.0+ Running Shoe 10.15 55.82 A 358.365
48 Nike Men's Dri-FIT Victory Golf Polo 9.47 65.29 A 334.163
59 Pelican Sunstream 100 Kayak 8.71 74.00 A 307.523
56 O'Brien Men's Neoprene Life Vest 8.56 82.56 B 302.191
45 Nike Men's CJ Elite 2 TD Football Cleat 8.40 90.96 B 296.357
85 Under Armour Girls' Toddler Spine Surge Runni 3.44 94.40 B 121.569
96 adidas Men's F10 Messi TRX FG Soccer Cleat 0.23 94.63 B 8.078
98 adidas Youth Germany Black/Red Away Match Soc 0.21 94.84 B 7.468
81 Titleist Pro V1x High Numbers Personalized Go 0.18 95.02 C 6.226
35 LIJA Women's Eyelet Sleeveless Golf Polo 0.18 95.20 C 6.391
46 Nike Men's Comfort 2 Slide 0.17 95.37 C 5.970
78 Titleist Pro V1 High Numbers Personalized Gol 0.17 95.54 C 6.004
80 Titleist Pro V1x High Numbers Golf Balls 0.12 95.66 C 4.376
66 TYR Boys' Team Digi Jammer 0.12 95.78 C 4.119
86 Under Armour Hustle Storm Medium Duffle Bag 0.12 95.90 C 4.097
92 Under Armour Women's Micro G Skulpt Running S 0.12 96.02 C 4.084
79 Titleist Pro V1x Golf Balls 0.10 96.12 C 3.494
In [170]:
fig = px.bar(nome_produto_lucro_ordenado[nome_produto_lucro_ordenado['Categoria'] == 'A'], x='Nome do Produto', y='Lucros Totais (em milhares)', color='Nome do Produto',
                 title="Produtos que correspondem a 80% do valor total do lucro", text = 'Lucros Totais (em milhares)')

fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()

Os produtos do gráfico acima correspondem a 80% das vendas (em valores financeiros) da DataCo Global:

  • Field & Stream Sportsman 16 Gun Fire Safe
  • Perfect Fitness Perfect Rip Deck
  • Diamondback Women's Serene Classic Comfort Bi
  • Nike Men's Free 5.0+ Running Shoe
  • Nike Men's Dri-FIT Victory Golf Polo
  • Pelican Sunstream 100 Kayak
  • O'Brien Men's Neoprene Life Vest

11. Modelagem - Previsão de Entregas em Atraso

Objetivo: Prever se um pedido será entregue com atraso, o que é importante para a gestão de expectativas dos clientes e planejamento logístico.

In [171]:
df_filtrado.info()
<class 'pandas.core.frame.DataFrame'>
Index: 164572 entries, 48 to 180518
Data columns (total 55 columns):
 #   Column                         Non-Null Count   Dtype         
---  ------                         --------------   -----         
 0   Type                           164572 non-null  object        
 1   Days for shipping (real)       164572 non-null  int64         
 2   Days for shipment (scheduled)  164572 non-null  int64         
 3   Benefit per order              164572 non-null  float64       
 4   Sales per customer             164572 non-null  float64       
 5   Delivery Status                164572 non-null  object        
 6   Late_delivery_risk             164572 non-null  int64         
 7   Category Id                    164572 non-null  int64         
 8   Category Name                  164572 non-null  object        
 9   Customer City                  164572 non-null  object        
 10  Customer Country               164572 non-null  object        
 11  Customer Email                 164572 non-null  object        
 12  Customer Fname                 164572 non-null  object        
 13  Customer Id                    164572 non-null  int64         
 14  Customer Lname                 164572 non-null  object        
 15  Customer Password              164572 non-null  object        
 16  Customer Segment               164572 non-null  object        
 17  Customer State                 164572 non-null  object        
 18  Customer Street                164572 non-null  object        
 19  Customer Zipcode               164572 non-null  float64       
 20  Department Id                  164572 non-null  int64         
 21  Department Name                164572 non-null  object        
 22  Latitude                       164572 non-null  float64       
 23  Longitude                      164572 non-null  float64       
 24  Market                         164572 non-null  object        
 25  Order City                     164572 non-null  object        
 26  Order Country                  164572 non-null  object        
 27  Order Customer Id              164572 non-null  int64         
 28  order date (DateOrders)        164572 non-null  datetime64[ns]
 29  Order Id                       164572 non-null  int64         
 30  Order Item Cardprod Id         164572 non-null  int64         
 31  Order Item Discount            164572 non-null  float64       
 32  Order Item Discount Rate       164572 non-null  float64       
 33  Order Item Id                  164572 non-null  int64         
 34  Order Item Product Price       164572 non-null  float64       
 35  Order Item Profit Ratio        164572 non-null  float64       
 36  Order Item Quantity            164572 non-null  int64         
 37  Sales                          164572 non-null  float64       
 38  Order Item Total               164572 non-null  float64       
 39  Order Profit Per Order         164572 non-null  float64       
 40  Order Region                   164572 non-null  object        
 41  Order State                    164572 non-null  object        
 42  Order Status                   164572 non-null  object        
 43  Order Zipcode                  23720 non-null   float64       
 44  Product Card Id                164572 non-null  int64         
 45  Product Category Id            164572 non-null  int64         
 46  Product Description            0 non-null       float64       
 47  Product Image                  164572 non-null  object        
 48  Product Name                   164572 non-null  object        
 49  Product Price                  164572 non-null  float64       
 50  Product Status                 164572 non-null  int64         
 51  shipping date (DateOrders)     164572 non-null  object        
 52  Shipping Mode                  164572 non-null  object        
 53  Profit Margin                  164572 non-null  float64       
 54  Atraso                         164572 non-null  int32         
dtypes: datetime64[ns](1), float64(16), int32(1), int64(14), object(23)
memory usage: 69.7+ MB
In [172]:
# Criando a coluna target
df_filtrado['target'] = (df_filtrado['Days for shipping (real)'] > df_filtrado['Days for shipment (scheduled)']).astype(int)
In [173]:
# Removendo as colunas desnecessárias

colunas_para_remover = ['Days for shipping (real)', 'Days for shipment (scheduled)', 'Category Name', 'Customer Email', 'Customer Fname',
                       'Customer Id', 'Customer Lname', 'Customer Password', 'Customer Street', 'Customer Zipcode',
                       'Department Name', 'Latitude', 'Longitude', 'Order Customer Id', 'Order Id', 
                       'Order Item Cardprod Id', 'Order Item Id', 'Order Profit Per Order', 'Order Zipcode', 'Product Card Id',
                       'Product Description', 'Product Image', 'Product Price', 'Product Status', 'shipping date (DateOrders)',
                       'Late_delivery_risk', 'Delivery Status', 'Profit Margin', 'Atraso', 'order date (DateOrders)']

df_filtrado.drop(columns=colunas_para_remover, inplace=True)

df_filtrado.head()
Out[173]:
Type Benefit per order Sales per customer Category Id Customer City Customer Country Customer Segment Customer State Department Id Market Order City Order Country Order Item Discount Order Item Discount Rate Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Region Order State Order Status Product Category Id Product Name Shipping Mode target
48 PAYMENT -30.750000 115.180000 17 Bayamon Puerto Rico Home Office PR 4 Pacific Asia Mirzapur India 4.8 0.04 59.990002 -0.27 2 119.980003 115.180000 South Asia Uttar Pradesh PENDING_PAYMENT 17 Perfect Fitness Perfect Rip Deck Second Class 1
49 PAYMENT -122.730003 79.180000 29 Caguas Puerto Rico Home Office PR 5 Pacific Asia Bursa Turquía 0.8 0.01 39.990002 -1.55 2 79.980003 79.180000 West Asia Bursa PENDING_PAYMENT 29 Under Armour Girls' Toddler Spine Surge Runni Second Class 0
50 PAYMENT 33.599998 96.000000 24 Caguas Puerto Rico Home Office PR 5 Pacific Asia Murray Bridge Australia 4.0 0.04 50.000000 0.35 2 100.000000 96.000000 Oceania Australia del Sur PENDING_PAYMENT 24 Nike Men's Dri-FIT Victory Golf Polo Second Class 1
51 PAYMENT 24.690001 75.980003 29 Caguas Puerto Rico Home Office PR 5 Pacific Asia Kartal Turquía 4.0 0.05 39.990002 0.33 2 79.980003 75.980003 West Asia Estambul PENDING_PAYMENT 29 Under Armour Girls' Toddler Spine Surge Runni Second Class 0
52 PAYMENT 9.100000 91.000000 24 Caguas Puerto Rico Home Office PR 5 Pacific Asia Ulan Bator Mongolia 9.0 0.09 50.000000 0.10 2 100.000000 91.000000 Eastern Asia Ulán Bator PENDING_PAYMENT 24 Nike Men's Dri-FIT Victory Golf Polo Second Class 1
In [174]:
# Porcentagem pedidos atrasados (1) e não atrasados (0) em todo o dataset
df_filtrado['target'].value_counts(1)
Out[174]:
target
1    0.572807
0    0.427193
Name: proportion, dtype: float64
In [175]:
# class weight
weights = df_filtrado.target.value_counts(1)[0]/df_filtrado.target.value_counts(1)[1]
In [176]:
# Divisão em X e y
X = df_filtrado.drop(columns=['target'], axis = 1)
y = df_filtrado.target
In [177]:
# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state = 42, stratify=y)
In [178]:
# Porcentagem pedidos atrasados (1) e não atrasados (0) no conjunto de treino
y_train.value_counts(1)
Out[178]:
target
1    0.572804
0    0.427196
Name: proportion, dtype: float64
In [179]:
# Porcentagem pedidos atrasados (1) e não atrasados (0) no conjunto de teste
y_test.value_counts(1)
Out[179]:
target
1    0.572815
0    0.427185
Name: proportion, dtype: float64
In [180]:
# Instanciando modelo XGBoost
modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')

# Instanciando modelo LightGBM
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = 8, num_leaves = 2^8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, is_unbalance=True, verbose=-1)

# Instanciando modelo catboost
modelo_CatBoost = CatBoostClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, random_state = 0, scale_pos_weight = weights, verbose = 0)

# Instanciando o modelo Balanced Random Forest
modelo_BRandom_Forest = BalancedRandomForestClassifier(n_estimators = 1000, max_depth = 8, random_state = 0, verbose = 0)

Construção de uma função de validação cruzada - Stratified K-Fold¶

In [181]:
# Função para aplicação da validação cruzada para obtenção das métricas dos modelos

def validacao_cruzada(X, y, modelo, k, threshold):

    # Inicializando a função StratifiedKFold
    folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=40)

    # Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
    # em cada fold

    precisoes = list()
    revocacoes=list()
    acuracias=list()
    Medida_F1=list()
    precision_recall_auc=list()
    rocs_auc=list()
    cm_total = np.zeros((2, 2))
    
    # Será aplicado o método "split" no objeto folds, que retornará uma lista 
    # com os índices das instâncias que pertencem ao conjunto de treino e 
    # outra com os índices das instâncias que pertencem ao conjunto de teste
    
    for k, (train_index, test_index) in enumerate(folds.split(X,y)):
        print("=-"*6 + f"Fold: {k+1}" + "-="*6)
        
        # Dividindo os dados em treino e teste para cada um dos folds
        X_train_intern, y_train_intern = X.iloc[train_index, :], y.iloc[train_index]
        X_test_intern, y_test_intern = X.iloc[test_index, :], y.iloc[test_index]
        
        # train_index e test_index: São os índices das instâncias do conjunto 
        # de treino e teste, respectivamente, selecionados em cada um dos folds
        
        ###########################################
        ############## Preprocessing ##############
        ###########################################
    
        # Instanciando o CatBoost Encoder        
        encoder = CatBoostEncoder()

        # Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
        cat_imputer = SimpleImputer(strategy='most_frequent')
        
        # Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
        num_imputer = SimpleImputer(strategy='median')

        # Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
        cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
        num_pipeline = Pipeline([('imputer', num_imputer)])

        # Identifica as variáveis categóricas e numéricas
        cat_cols = X_train_intern.select_dtypes(include=['object']).columns
        num_cols = X_train_intern.select_dtypes(exclude=['object']).columns

        # Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
        X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
        X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])

        # Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
        X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
        X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])

        # Treinando o modelo
        modelo.fit(X_train_intern, y_train_intern)

        # Obtendo as probabilidades de cada registro pertencer a classe 1
        y_pred_proba = modelo.predict_proba(X_test_intern)[:, 1]

        # Obtendo as previsões do modelo
        y_pred = np.where(y_pred_proba > threshold, 1, 0)

        # Calculando a precisão e revocação para determinar a precision_recall_auc
        precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)

        # Calculando a matriz de confusão do fold
        cm_total += confusion_matrix(y_test_intern, y_pred)

        # Determinando as métricas para cada fold
        precisao_revocacao_auc = auc(revocacao, precisao)
        roc_auc = roc_auc_score(y_test_intern, y_pred)
        acuracia_score = accuracy_score(y_test_intern, y_pred)
        precisao_score = precision_score(y_test_intern, y_pred)
        revocacao_score = recall_score(y_test_intern, y_pred)
        f1score = f1_score(y_test_intern, y_pred)

        # Armazenando as métricas nas listas criadas
        precisoes.append(precisao_score)
        revocacoes.append(revocacao_score)
        precision_recall_auc.append(precisao_revocacao_auc)
        rocs_auc.append(roc_auc)
        acuracias.append(acuracia_score)
        Medida_F1.append(f1score)

        # Exibindo as métricas para cada um dos folds
        print(f"Precisão: {precisao_score:.4f}")
        print(f"Revocação: {revocacao_score:.4f}")
        print(f"Acurácia: {acuracia_score:.4f}")
        print(f"Medida F1: {f1score:.4f}")
        print(f"Precision-Recall AUC: {precisao_revocacao_auc:.4f}")
        print(f"ROC AUC: {roc_auc:.4f}")

    # Transformando as listas em arrays para fazer operações matemáticas
    precisoes = np.array(precisoes)
    revocacoes = np.array(revocacoes)
    precision_recall_auc = np.array(precision_recall_auc)
    rocs_auc = np.array(rocs_auc)
    acuracias = np.array(acuracias)
    Medida_F1 = np.array(Medida_F1)

    # Calculando as médias das métricas
    media_revocacao = np.mean(revocacoes)
    media_precisao = np.mean(precisoes)
    media_acuracia = np.mean(acuracias)
    media_F1 = np.mean(Medida_F1)
    media_pr_AUC = np.mean(precision_recall_auc)
    media_roc_AUC = np.mean(rocs_auc)

    # Calculando os desvios padrão para cada métrica
    std_revocacao = np.std(revocacoes)
    std_precisao = np.std(precisoes)
    std_acuracia = np.std(acuracias)
    std_F1 = np.std(Medida_F1)
    std_pr_AUC = np.std(precision_recall_auc)
    std_roc_AUC = np.std(rocs_auc)

    # Exibindo as médias das métricas obtidas
    print()
    print("=-"*6 + "Exibindo a média das métricas obtidas" + "-="*6)
    print(f"Média da acurácia: {media_acuracia:.4f} +/- {std_acuracia:.4f}")
    print(f"Média da revocação: {media_revocacao:.4f} +/- {std_revocacao:.4f}")
    print(f"Média da precisão: {media_precisao:.4f} +/- {std_precisao:.4f}")
    print(f"Média da Medida F1: {media_F1:.4f} +/- {std_F1:.4f}")
    print(f"Média da ROC AUC: {media_roc_AUC:.4f} +/- {std_roc_AUC:.4f}")
    print(f"Média da PR AUC: {media_pr_AUC:.4f} +/- {std_pr_AUC:.4f}")

    # Plotando a matriz de confusão agregada com heatmap
    plt.figure(figsize=(8, 6))
    sns.heatmap(cm_total, annot=True, fmt=".0f", cmap="Blues")
    plt.title("Matriz de Confusão Agregada de Todos os Folds")
    plt.ylabel('Verdadeiro')
    plt.xlabel('Previsto')
    plt.show()

Modelo LightGBM¶

In [182]:
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8790
Revocação: 0.5806
Acurácia: 0.7140
Medida F1: 0.6993
Precision-Recall AUC: 0.8499
ROC AUC: 0.7367
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8707
Revocação: 0.5741
Acurácia: 0.7072
Medida F1: 0.6919
Precision-Recall AUC: 0.8444
ROC AUC: 0.7299
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8751
Revocação: 0.5787
Acurácia: 0.7113
Medida F1: 0.6967
Precision-Recall AUC: 0.8475
ROC AUC: 0.7339
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8766
Revocação: 0.5797
Acurácia: 0.7125
Medida F1: 0.6979
Precision-Recall AUC: 0.8485
ROC AUC: 0.7351
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8715
Revocação: 0.5744
Acurácia: 0.7077
Medida F1: 0.6924
Precision-Recall AUC: 0.8449
ROC AUC: 0.7304

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7105 +/- 0.0027
Média da revocação: 0.5775 +/- 0.0027
Média da precisão: 0.8746 +/- 0.0031
Média da Medida F1: 0.6956 +/- 0.0029
Média da ROC AUC: 0.7332 +/- 0.0027
Média da PR AUC: 0.8470 +/- 0.0021
No description has been provided for this image

Modelo XGBoost¶

In [183]:
validacao_cruzada(X, y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8833
Revocação: 0.5780
Acurácia: 0.7145
Medida F1: 0.6987
Precision-Recall AUC: 0.8515
ROC AUC: 0.7378
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8786
Revocação: 0.5657
Acurácia: 0.7064
Medida F1: 0.6882
Precision-Recall AUC: 0.8465
ROC AUC: 0.7304
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8788
Revocação: 0.5771
Acurácia: 0.7122
Medida F1: 0.6967
Precision-Recall AUC: 0.8491
ROC AUC: 0.7352
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8858
Revocação: 0.5709
Acurácia: 0.7121
Medida F1: 0.6943
Precision-Recall AUC: 0.8512
ROC AUC: 0.7361
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8714
Revocação: 0.5788
Acurácia: 0.7098
Medida F1: 0.6956
Precision-Recall AUC: 0.8457
ROC AUC: 0.7321

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7110 +/- 0.0027
Média da revocação: 0.5741 +/- 0.0050
Média da precisão: 0.8796 +/- 0.0049
Média da Medida F1: 0.6947 +/- 0.0036
Média da ROC AUC: 0.7343 +/- 0.0027
Média da PR AUC: 0.8488 +/- 0.0024
No description has been provided for this image

Modelo CatBoost¶

In [184]:
validacao_cruzada(X, y, modelo_CatBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8885
Revocação: 0.5721
Acurácia: 0.7138
Medida F1: 0.6960
Precision-Recall AUC: 0.8529
ROC AUC: 0.7379
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8772
Revocação: 0.5658
Acurácia: 0.7059
Medida F1: 0.6879
Precision-Recall AUC: 0.8459
ROC AUC: 0.7298
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8826
Revocação: 0.5705
Acurácia: 0.7105
Medida F1: 0.6931
Precision-Recall AUC: 0.8496
ROC AUC: 0.7344
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8858
Revocação: 0.5685
Acurácia: 0.7109
Medida F1: 0.6925
Precision-Recall AUC: 0.8507
ROC AUC: 0.7351
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8801
Revocação: 0.5648
Acurácia: 0.7067
Medida F1: 0.6881
Precision-Recall AUC: 0.8471
ROC AUC: 0.7308

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7095 +/- 0.0029
Média da revocação: 0.5683 +/- 0.0027
Média da precisão: 0.8829 +/- 0.0040
Média da Medida F1: 0.6915 +/- 0.0031
Média da ROC AUC: 0.7336 +/- 0.0030
Média da PR AUC: 0.8492 +/- 0.0025
No description has been provided for this image

Modelo Balanced Random Forest¶

In [185]:
validacao_cruzada(X, y, modelo_BRandom_Forest, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8828
Revocação: 0.5766
Acurácia: 0.7136
Medida F1: 0.6976
Precision-Recall AUC: 0.8510
ROC AUC: 0.7370
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8778
Revocação: 0.5652
Acurácia: 0.7058
Medida F1: 0.6876
Precision-Recall AUC: 0.8460
ROC AUC: 0.7298
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8819
Revocação: 0.5705
Acurácia: 0.7102
Medida F1: 0.6928
Precision-Recall AUC: 0.8492
ROC AUC: 0.7340
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8857
Revocação: 0.5674
Acurácia: 0.7103
Medida F1: 0.6917
Precision-Recall AUC: 0.8505
ROC AUC: 0.7346
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8795
Revocação: 0.5654
Acurácia: 0.7067
Medida F1: 0.6883
Precision-Recall AUC: 0.8469
ROC AUC: 0.7308

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7093 +/- 0.0028
Média da revocação: 0.5690 +/- 0.0042
Média da precisão: 0.8816 +/- 0.0027
Média da Medida F1: 0.6916 +/- 0.0036
Média da ROC AUC: 0.7333 +/- 0.0026
Média da PR AUC: 0.8487 +/- 0.0019
No description has been provided for this image

Dentre todas as métricas de avaliação, será dada prioridade para a ROC AUC, que mede a capacidade do modelo distinguir entre entregas atrasadas e não atrasadas. Caso o custo de uma entrega em atraso não identificada previamente (falso negativo) seja alto, esta métrica se torna uma grande arma para reduzir custos operacionais e aumentar o lucro da empresa.

Possíveis impactos de constantes atrasos em entrega¶

1) Satisfação do Cliente

  • Expectativa dos clientes: Atrasos não identificados e não comunicados podem levar à insatisfação do cliente, quebra de confiança e potencial perda de clientes.
  • Reputação online: Uma entrega atrasada pode levar a avaliações negativas em redes sociais, podendo causar danos à reputação da empresa perante potenciais clientes.

2) Custos financeiros diretos

  • Custos de Remessa e Logística: Atrasos podem aumentar os custos de logística, especialmente se forem necessárias medidas corretivas, como reexpedição ou entrega expressa.
  • Penalidades Contratuais: Em alguns casos, atrasos nas entregas podem levar a multas ou penalidades contratuais, especialmente em negócios B2B, onde os contratos são mais rigorosos.

3) Implicações de Longo Prazo

  • Relações com Clientes B2B: No caso de clientes empresariais, os atrasos nas entregas podem interromper suas operações, prejudicando a relação comercial de longo prazo, consequentemente as fontes de receita ao longo prazo.
  • Efeito Cascata na Cadeia de Suprimentos: Atrasos em um nó da cadeia podem afetar outros, especialmente em um modelo just-in-time, onde os produtos são produzidos ou entregues exatamente quando necessários.

4) Estratégias Competitivas

  • Perda de Vantagem Competitiva: Em um mercado global, a capacidade de entregar no prazo pode ser um diferencial competitivo. Atrasos frequentes podem abrir margem para o crescimento de concorrentes.
  • Perda de Mercado: Clientes insatisfeitos podem se voltar para concorrentes com histórico de entregas mais confiáveis.

Sendo assim, considerando o impacto financeiro e operacional dos atrasos em entregas, é fundamental para uma empresa de supply chain como a DataCo Global, com presença global, ter uma logística capaz de cumprir com prazos e metas.

Na modelagem de previsão, a priorização da ROC AUC é uma estratégia-chave para isso. Com uma ROC AUC elevada, a empresa consegue melhor classificar entre pedidos com alta probabilidade de atraso e aqueles que provavelmente serão entregues no prazo, permitindo-lhes tomar medidas para mitigar os impactos negativos. Isso pode incluir desde a comunicação antecipada com os clientes sobre potenciais atrasos, reajustes na logística para acelerar entregas subsequentes, ou alterações na gestão do estoque para lidar com possíveis interrupções na cadeia de suprimentos.

Dentre os modelos escolhidos para avaliar a métrica ROC AUC, o XGBoost foi o que melhor performou, com uma ROC AUC média de 0.7344, ou seja, significa que existe 73,44% de chance de que o modelo classifique corretamente um pedido aleatório atrasado como atrasado e um pedido aleatório não atrasado como não atrasado.

Como o LightGBM atingiu uma ROC AUC praticamente idêntica a do XGBoost e, possui um processamento mais rápido que o XGBoost, será escolhido o LightGBM para passar por um processo de tunagem de hiperparâmetros.

Feature Selection¶

In [186]:
# Inicializando o RFE
rfe = RFE(estimator = modelo_LightGBM, n_features_to_select = 20, step = 1)

encoder = CatBoostEncoder()
# Ajustar e transformar os dados de treinamento
X_train_encoded = encoder.fit_transform(X_train, y_train)

# Transformar os dados de teste
X_test_encoded = encoder.transform(X_test)

# Treinando o RFE
rfe.fit(X_train_encoded, y_train)

# Obtendo as features selecionadas
features_importantes = np.array(list(X_train_encoded.columns))[rfe.support_]

features_importantes
Out[186]:
array(['Type', 'Benefit per order', 'Sales per customer', 'Category Id',
       'Customer City', 'Customer Country', 'Customer Segment',
       'Customer State', 'Department Id', 'Order City',
       'Order Item Profit Ratio', 'Order Item Quantity', 'Sales',
       'Order Item Total', 'Order Region', 'Order State', 'Order Status',
       'Product Category Id', 'Product Name', 'Shipping Mode'],
      dtype='<U24')
In [187]:
validacao_cruzada(X[features_importantes], y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8790
Revocação: 0.5806
Acurácia: 0.7140
Medida F1: 0.6993
Precision-Recall AUC: 0.8499
ROC AUC: 0.7367
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8707
Revocação: 0.5741
Acurácia: 0.7072
Medida F1: 0.6919
Precision-Recall AUC: 0.8444
ROC AUC: 0.7299
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8751
Revocação: 0.5787
Acurácia: 0.7113
Medida F1: 0.6967
Precision-Recall AUC: 0.8475
ROC AUC: 0.7339
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8766
Revocação: 0.5797
Acurácia: 0.7125
Medida F1: 0.6979
Precision-Recall AUC: 0.8485
ROC AUC: 0.7351
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8715
Revocação: 0.5744
Acurácia: 0.7077
Medida F1: 0.6924
Precision-Recall AUC: 0.8449
ROC AUC: 0.7304

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7105 +/- 0.0027
Média da revocação: 0.5775 +/- 0.0027
Média da precisão: 0.8746 +/- 0.0031
Média da Medida F1: 0.6956 +/- 0.0029
Média da ROC AUC: 0.7332 +/- 0.0027
Média da PR AUC: 0.8470 +/- 0.0021
No description has been provided for this image

Neste caso, realizando uma feature selection e excluindo as 5 colunas menos importantes para o modelos, atingimos o mesmo resultado que com o dataset original. Assim, por questões de processamento, é preferível manter o dataset com as 5 colunas a menos.

Tunagem de Hiperparâmetros¶

In [188]:
def tunagem_hiperparametros(trial, k = 5, threshold = 0.5):

    # Parâmetros para serem tunados
    learning_rate = trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True)
    max_depth = trial.suggest_int('max_depth', 1, 20)
    subsample = trial.suggest_float('subsample', 0.5, 1, step = 0.1)
    colsample_bytree = trial.suggest_float('colsample_bytree', 0.5, 1, step = 0.1)
    min_child_samples = trial.suggest_int('min_child_samples', 1, 20)
    min_child_weight = trial.suggest_float('min_child_weight', 1e-3, 1e-1)
    
    # Inicializando a função StratifiedKFold
    folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)

    # Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
    # em cada fold
    precisoes = list()
    revocacoes=list()
    acuracias=list()
    Medida_F1=list()
    precision_recall_auc=list()
    rocs_auc=list()

    # Será aplicado o método "split" no objeto folds, que retornará uma lista 
    # com os índices das instâncias que pertencem ao conjunto de treino e 
    # outra com os índices das instâncias que pertencem ao conjunto de teste

    for k, (train_index, test_index) in enumerate(folds.split(X[features_importantes],y)):
        print("=-"*6 + f"Fold: {k+1}" + "-="*6)

        # Dividindo os dados em treino e teste para cada um dos folds
        X_train_intern, y_train_intern = X[features_importantes].iloc[train_index, :], y.iloc[train_index]
        X_test_intern, y_test_intern = X[features_importantes].iloc[test_index, :], y.iloc[test_index]

        # train_index e test_index: São os índices das instâncias do conjunto 
        # de treino e teste, respectivamente, selecionados em cada um dos folds
        
        ###########################################
        ############## Preprocessing ##############
        ###########################################
    
        # Instanciando o CatBoost Encoder        
        encoder = CatBoostEncoder()

        # Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
        cat_imputer = SimpleImputer(strategy='most_frequent')
        
        # Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
        num_imputer = SimpleImputer(strategy='median')

        # Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
        cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
        num_pipeline = Pipeline([('imputer', num_imputer)])

        # Identifica as variáveis categóricas e numéricas
        cat_cols = X_train_intern.select_dtypes(include=['object']).columns
        num_cols = X_train_intern.select_dtypes(exclude=['object']).columns

        # Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
        X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
        X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])

        # Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
        X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
        X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])

        # Instanciando o Modelo LightGBM
        modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = max_depth, num_leaves = 2^8, subsample = subsample, 
                                         min_child_weight = min_child_weight, min_child_samples = min_child_samples, 
                                         colsample_bytree = colsample_bytree, learning_rate = learning_rate, n_jobs =-1,
                                         random_state = 0, is_unbalance=True, verbose=-1)
        
        # Treinando o modelo LightGBM
        modelo_LightGBM.fit(X_train_intern, y_train_intern)

        # Obtendo as probabilidades de cada registro pertencer a classe 1
        y_pred_proba = modelo_LightGBM.predict_proba(X_test_intern)[:, 1]

        # Obtendo as previsões do modelo
        y_pred = np.where(y_pred_proba > threshold, 1, 0)

        # Calculando a precisão e revocação para determinar a precision_recall_auc
        precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)

        # Determinando as métricas para cada fold
        precisao_revocacao_auc = auc(revocacao, precisao)
        roc_auc = roc_auc_score(y_test_intern, y_pred)
        acuracia_score = accuracy_score(y_test_intern, y_pred)
        precisao_score = precision_score(y_test_intern, y_pred)
        revocacao_score = recall_score(y_test_intern, y_pred)
        f1score = f1_score(y_test_intern, y_pred)

        # Armazenando as métricas nas listas criadas
        precisoes.append(precisao_score)
        revocacoes.append(revocacao_score)
        precision_recall_auc.append(precisao_revocacao_auc)
        rocs_auc.append(roc_auc)
        acuracias.append(acuracia_score)
        Medida_F1.append(f1score)

    # Transformando as listas em arrays para fazer operações matemáticas
    precisoes = np.array(precisoes)
    revocacoes = np.array(revocacoes)
    precision_recall_auc = np.array(precision_recall_auc)
    rocs_auc = np.array(rocs_auc)
    acuracias = np.array(acuracias)
    Medida_F1 = np.array(Medida_F1)

    # Calculando as médias das métricas
    media_revocacao = np.mean(revocacoes)
    media_precisao = np.mean(precisoes)
    media_acuracia = np.mean(acuracias)
    media_F1 = np.mean(Medida_F1)
    media_pr_AUC = np.mean(precision_recall_auc)
    media_roc_AUC = np.mean(rocs_auc)

    # Calculando os desvios padrão para cada métrica
    std_revocacao = np.std(revocacoes)
    std_precisao = np.std(precisoes)
    std_acuracia = np.std(acuracias)
    std_F1 = np.std(Medida_F1)
    std_pr_AUC = np.std(precision_recall_auc)
    std_roc_AUC = np.std(rocs_auc)
    
    return media_roc_AUC


study = opt.create_study(direction='maximize')
study.optimize(tunagem_hiperparametros, n_trials = 20)
[I 2024-02-10 22:09:26,049] A new study created in memory with name: no-name-7a27c5dc-b174-4b96-9e7d-b473b524cd28
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:10:42,568] Trial 0 finished with value: 0.732976869219396 and parameters: {'learning_rate': 0.0020404155935778907, 'max_depth': 12, 'subsample': 0.5, 'colsample_bytree': 1.0, 'min_child_samples': 8, 'min_child_weight': 0.09324004081596833}. Best is trial 0 with value: 0.732976869219396.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:11:39,538] Trial 1 finished with value: 0.7339915999812193 and parameters: {'learning_rate': 0.011507553799106215, 'max_depth': 5, 'subsample': 0.6, 'colsample_bytree': 0.6, 'min_child_samples': 4, 'min_child_weight': 0.06489413252586655}. Best is trial 1 with value: 0.7339915999812193.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:12:35,621] Trial 2 finished with value: 0.7343121783678607 and parameters: {'learning_rate': 0.0668214382430312, 'max_depth': 13, 'subsample': 0.5, 'colsample_bytree': 0.5, 'min_child_samples': 2, 'min_child_weight': 0.054893193695231275}. Best is trial 2 with value: 0.7343121783678607.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:13:37,662] Trial 3 finished with value: 0.7341373624562854 and parameters: {'learning_rate': 0.027897146878269743, 'max_depth': 14, 'subsample': 0.7, 'colsample_bytree': 0.9, 'min_child_samples': 12, 'min_child_weight': 0.09311420300985243}. Best is trial 2 with value: 0.7343121783678607.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:14:34,324] Trial 4 finished with value: 0.7343392890177047 and parameters: {'learning_rate': 0.05139440060878258, 'max_depth': 9, 'subsample': 0.6, 'colsample_bytree': 0.5, 'min_child_samples': 15, 'min_child_weight': 0.05113215031321493}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:15:27,249] Trial 5 finished with value: 0.7343173450751701 and parameters: {'learning_rate': 0.041001003249000106, 'max_depth': 9, 'subsample': 1.0, 'colsample_bytree': 0.7, 'min_child_samples': 5, 'min_child_weight': 0.009030050793958331}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:16:07,893] Trial 6 finished with value: 0.7340343882613765 and parameters: {'learning_rate': 0.07323321762143926, 'max_depth': 2, 'subsample': 0.8, 'colsample_bytree': 0.6, 'min_child_samples': 14, 'min_child_weight': 0.0212868551464531}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:17:09,167] Trial 7 finished with value: 0.7333645247162839 and parameters: {'learning_rate': 0.0024513249648152264, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_samples': 1, 'min_child_weight': 0.06107187658125102}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:18:06,027] Trial 8 finished with value: 0.734043454313138 and parameters: {'learning_rate': 0.026373724315410213, 'max_depth': 12, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_samples': 9, 'min_child_weight': 0.03775035246208278}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:18:59,343] Trial 9 finished with value: 0.7341953321962944 and parameters: {'learning_rate': 0.03235637623662865, 'max_depth': 9, 'subsample': 0.5, 'colsample_bytree': 0.5, 'min_child_samples': 1, 'min_child_weight': 0.016465912415566535}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:20:01,351] Trial 10 finished with value: 0.7339768956495942 and parameters: {'learning_rate': 0.007877859342979632, 'max_depth': 19, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_samples': 20, 'min_child_weight': 0.040042373755256974}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:21:01,827] Trial 11 finished with value: 0.734069803750568 and parameters: {'learning_rate': 0.09502736701688062, 'max_depth': 8, 'subsample': 1.0, 'colsample_bytree': 0.7, 'min_child_samples': 16, 'min_child_weight': 0.002451776114696984}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:22:01,092] Trial 12 finished with value: 0.7341169736666421 and parameters: {'learning_rate': 0.013739306362388414, 'max_depth': 7, 'subsample': 0.9, 'colsample_bytree': 0.6, 'min_child_samples': 6, 'min_child_weight': 0.07325873711793982}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:23:11,248] Trial 13 finished with value: 0.7342176615957454 and parameters: {'learning_rate': 0.044968600068135314, 'max_depth': 16, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_samples': 18, 'min_child_weight': 0.03486072970401499}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:24:35,996] Trial 14 finished with value: 0.7338019752137341 and parameters: {'learning_rate': 0.005228658800279436, 'max_depth': 10, 'subsample': 0.9, 'colsample_bytree': 0.5, 'min_child_samples': 12, 'min_child_weight': 0.07605837304718703}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:26:05,548] Trial 15 finished with value: 0.7339278495596633 and parameters: {'learning_rate': 0.01716729776084651, 'max_depth': 5, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_samples': 15, 'min_child_weight': 0.0013061193531905407}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:27:21,225] Trial 16 finished with value: 0.7341814782844065 and parameters: {'learning_rate': 0.045960965964675755, 'max_depth': 16, 'subsample': 0.9, 'colsample_bytree': 0.7, 'min_child_samples': 6, 'min_child_weight': 0.022016104377459617}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:28:45,591] Trial 17 finished with value: 0.7332943864225296 and parameters: {'learning_rate': 0.001146775199694044, 'max_depth': 7, 'subsample': 0.7, 'colsample_bytree': 0.6, 'min_child_samples': 10, 'min_child_weight': 0.04595357772517577}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:29:42,792] Trial 18 finished with value: 0.7340324656424106 and parameters: {'learning_rate': 0.020399245076261327, 'max_depth': 2, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_samples': 12, 'min_child_weight': 0.0290828869584896}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:30:47,842] Trial 19 finished with value: 0.7338228285426818 and parameters: {'learning_rate': 0.007093739073996733, 'max_depth': 10, 'subsample': 1.0, 'colsample_bytree': 0.5, 'min_child_samples': 7, 'min_child_weight': 0.011942887474184281}. Best is trial 4 with value: 0.7343392890177047.
In [189]:
# Melhores parâmetros obtidos do último Trial
params = {'learning_rate': 0.06672510713241127, 'max_depth': 16, 'subsample': 1.0, 'colsample_bytree': 0.6, 'min_child_samples': 12, 'min_child_weight': 0.034638755930084086}
In [190]:
# LightGBM executado para os melhores parâmetros
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, num_leaves = 2^8, n_jobs =-1, random_state = 0, is_unbalance=True, **params, verbose=-1)
In [191]:
# Métricas do LightGBM utilizando os melhores parâmetros
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8859
Revocação: 0.5764
Acurácia: 0.7148
Medida F1: 0.6984
Precision-Recall AUC: 0.8525
ROC AUC: 0.7384
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8788
Revocação: 0.5643
Acurácia: 0.7058
Medida F1: 0.6873
Precision-Recall AUC: 0.8463
ROC AUC: 0.7300
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8830
Revocação: 0.5718
Acurácia: 0.7113
Medida F1: 0.6941
Precision-Recall AUC: 0.8501
ROC AUC: 0.7351
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8833
Revocação: 0.5725
Acurácia: 0.7118
Medida F1: 0.6948
Precision-Recall AUC: 0.8503
ROC AUC: 0.7356
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8840
Revocação: 0.5633
Acurácia: 0.7075
Medida F1: 0.6881
Precision-Recall AUC: 0.8487
ROC AUC: 0.7321

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7103 +/- 0.0032
Média da revocação: 0.5697 +/- 0.0050
Média da precisão: 0.8830 +/- 0.0023
Média da Medida F1: 0.6925 +/- 0.0042
Média da ROC AUC: 0.7342 +/- 0.0029
Média da PR AUC: 0.8496 +/- 0.0020
No description has been provided for this image

Após 20 iterações de tunagem de hiperparâmetros utilizando uma Bayesian Search, a ROC AUC média do modelo LightGBM melhorou pouco, de 73,32% para 73,41%. Sendo assim, tornou-se ainda melhor na identificação de pedidos com potencial entrega atrasada, ajudando a DataCo Global na identiicação precoce destes casos, contribuindo para mitigar os riscos decorrentes de problemas de logística.

Próximos passos: para melhorar ainda mais o desempenho do modelo, pode-se implementar técnicas de feature engineering, ou seja, criando outras variáveis à partir das já existentes, para buscar encontrar fatores que influenciem positivamente o modelo.

12. Modelagem - Previsão de Fraudes

Objetivo: Prever se um pedido poderá ser identificado como fraude, que é importante para evitar perdas financeiras à empresa.

In [192]:
df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 171962 entries, 48 to 180518
Data columns (total 54 columns):
 #   Column                         Non-Null Count   Dtype         
---  ------                         --------------   -----         
 0   Type                           171962 non-null  object        
 1   Days for shipping (real)       171962 non-null  int64         
 2   Days for shipment (scheduled)  171962 non-null  int64         
 3   Benefit per order              171962 non-null  float64       
 4   Sales per customer             171962 non-null  float64       
 5   Delivery Status                171962 non-null  object        
 6   Late_delivery_risk             171962 non-null  int64         
 7   Category Id                    171962 non-null  int64         
 8   Category Name                  171962 non-null  object        
 9   Customer City                  171962 non-null  object        
 10  Customer Country               171962 non-null  object        
 11  Customer Email                 171962 non-null  object        
 12  Customer Fname                 171962 non-null  object        
 13  Customer Id                    171962 non-null  int64         
 14  Customer Lname                 171962 non-null  object        
 15  Customer Password              171962 non-null  object        
 16  Customer Segment               171962 non-null  object        
 17  Customer State                 171962 non-null  object        
 18  Customer Street                171962 non-null  object        
 19  Customer Zipcode               171962 non-null  float64       
 20  Department Id                  171962 non-null  int64         
 21  Department Name                171962 non-null  object        
 22  Latitude                       171962 non-null  float64       
 23  Longitude                      171962 non-null  float64       
 24  Market                         171962 non-null  object        
 25  Order City                     171962 non-null  object        
 26  Order Country                  171962 non-null  object        
 27  Order Customer Id              171962 non-null  int64         
 28  order date (DateOrders)        171962 non-null  datetime64[ns]
 29  Order Id                       171962 non-null  int64         
 30  Order Item Cardprod Id         171962 non-null  int64         
 31  Order Item Discount            171962 non-null  float64       
 32  Order Item Discount Rate       171962 non-null  float64       
 33  Order Item Id                  171962 non-null  int64         
 34  Order Item Product Price       171962 non-null  float64       
 35  Order Item Profit Ratio        171962 non-null  float64       
 36  Order Item Quantity            171962 non-null  int64         
 37  Sales                          171962 non-null  float64       
 38  Order Item Total               171962 non-null  float64       
 39  Order Profit Per Order         171962 non-null  float64       
 40  Order Region                   171962 non-null  object        
 41  Order State                    171962 non-null  object        
 42  Order Status                   171962 non-null  object        
 43  Order Zipcode                  24840 non-null   float64       
 44  Product Card Id                171962 non-null  int64         
 45  Product Category Id            171962 non-null  int64         
 46  Product Description            0 non-null       float64       
 47  Product Image                  171962 non-null  object        
 48  Product Name                   171962 non-null  object        
 49  Product Price                  171962 non-null  float64       
 50  Product Status                 171962 non-null  int64         
 51  shipping date (DateOrders)     171962 non-null  object        
 52  Shipping Mode                  171962 non-null  object        
 53  target                         171962 non-null  int64         
dtypes: datetime64[ns](1), float64(15), int64(15), object(23)
memory usage: 76.2+ MB
In [193]:
# Criando a coluna target
df['target'] = df['Order Status'].apply(lambda x: 1 if x == 'SUSPECTED_FRAUD' else 0)
df.head()
Out[193]:
Type Days for shipping (real) Days for shipment (scheduled) Benefit per order Sales per customer Delivery Status Late_delivery_risk Category Id Category Name Customer City Customer Country Customer Email Customer Fname Customer Id Customer Lname Customer Password Customer Segment Customer State Customer Street Customer Zipcode Department Id Department Name Latitude Longitude Market Order City Order Country Order Customer Id order date (DateOrders) Order Id Order Item Cardprod Id Order Item Discount Order Item Discount Rate Order Item Id Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Profit Per Order Order Region Order State Order Status Order Zipcode Product Card Id Product Category Id Product Description Product Image Product Name Product Price Product Status shipping date (DateOrders) Shipping Mode target
48 PAYMENT 5 2 -30.750000 115.180000 Late delivery 1 17 Cleats Bayamon Puerto Rico XXXXXXXXX Mary 9083 Frank XXXXXXXXX Home Office PR 75 Sunny Grounds 957.0 4 Apparel 18.380119 -66.183128 Pacific Asia Mirzapur India 9083 2016-02-24 13:57:00 28744 365 4.8 0.04 71956 59.990002 -0.27 2 119.980003 115.180000 -30.750000 South Asia Uttar Pradesh PENDING_PAYMENT NaN 365 17 NaN http://images.acmesports.sports/Perfect+Fitness+Perfect+Rip+Deck Perfect Fitness Perfect Rip Deck 59.990002 0 2/29/2016 13:57 Second Class 0
49 PAYMENT 2 2 -122.730003 79.180000 Shipping on time 0 29 Shop By Sport Caguas Puerto Rico XXXXXXXXX Mary 4741 Smith XXXXXXXXX Home Office PR 9731 Honey Fox Towers 725.0 5 Golf 18.235573 -66.370613 Pacific Asia Bursa Turquía 4741 2016-10-25 14:39:00 45461 627 0.8 0.01 113598 39.990002 -1.55 2 79.980003 79.180000 -122.730003 West Asia Bursa PENDING_PAYMENT NaN 627 29 NaN http://images.acmesports.sports/Under+Armour+Girls%27+Toddler+Spine+Surge+Running+Shoe Under Armour Girls' Toddler Spine Surge Runni 39.990002 0 10/27/2016 14:39 Second Class 0
50 PAYMENT 6 2 33.599998 96.000000 Late delivery 1 24 Women's Apparel Caguas Puerto Rico XXXXXXXXX Elizabeth 639 Pittman XXXXXXXXX Home Office PR 7573 Golden Treasure Centre 725.0 5 Golf 18.025368 -66.613037 Pacific Asia Murray Bridge Australia 639 2016-03-30 04:37:00 31115 502 4.0 0.04 77757 50.000000 0.35 2 100.000000 96.000000 33.599998 Oceania Australia del Sur PENDING_PAYMENT NaN 502 24 NaN http://images.acmesports.sports/Nike+Men%27s+Dri-FIT+Victory+Golf+Polo Nike Men's Dri-FIT Victory Golf Polo 50.000000 0 4/5/2016 4:37 Second Class 0
51 PAYMENT 2 2 24.690001 75.980003 Shipping on time 0 29 Shop By Sport Caguas Puerto Rico XXXXXXXXX Katherine 9702 Tyler XXXXXXXXX Home Office PR 8369 Sunny Crossing 725.0 5 Golf 18.273838 -66.370636 Pacific Asia Kartal Turquía 9702 2016-10-30 01:31:00 45766 627 4.0 0.05 114401 39.990002 0.33 2 79.980003 75.980003 24.690001 West Asia Estambul PENDING_PAYMENT NaN 627 29 NaN http://images.acmesports.sports/Under+Armour+Girls%27+Toddler+Spine+Surge+Running+Shoe Under Armour Girls' Toddler Spine Surge Runni 39.990002 0 11/1/2016 1:31 Second Class 0
52 PAYMENT 3 2 9.100000 91.000000 Late delivery 1 24 Women's Apparel Caguas Puerto Rico XXXXXXXXX Mary 9114 Smith XXXXXXXXX Home Office PR 1425 Fallen Fox Arbor 725.0 5 Golf 18.284805 -66.370590 Pacific Asia Ulan Bator Mongolia 9114 2016-11-28 01:18:00 47752 502 9.0 0.09 119405 50.000000 0.10 2 100.000000 91.000000 9.100000 Eastern Asia Ulán Bator PENDING_PAYMENT NaN 502 24 NaN http://images.acmesports.sports/Nike+Men%27s+Dri-FIT+Victory+Golf+Polo Nike Men's Dri-FIT Victory Golf Polo 50.000000 0 12/1/2016 1:18 Second Class 0
In [194]:
# Removendo as colunas desnecessárias

colunas_para_remover = ['Days for shipping (real)', 'Days for shipment (scheduled)', 'Category Name', 'Customer Email', 'Customer Fname',
                       'Customer Id', 'Customer Lname', 'Customer Password', 'Customer Street', 'Customer Zipcode',
                       'Department Name', 'Latitude', 'Longitude', 'Order Customer Id', 'Order Id', 
                       'Order Item Cardprod Id', 'Order Item Id', 'Order Profit Per Order', 'Order Zipcode', 'Product Card Id',
                       'Product Description', 'Product Image', 'Product Status', 'shipping date (DateOrders)', 'order date (DateOrders)',
                       'Late_delivery_risk', 'Delivery Status', 'Order Status']

df.drop(columns=colunas_para_remover, inplace=True)

df.head()
Out[194]:
Type Benefit per order Sales per customer Category Id Customer City Customer Country Customer Segment Customer State Department Id Market Order City Order Country Order Item Discount Order Item Discount Rate Order Item Product Price Order Item Profit Ratio Order Item Quantity Sales Order Item Total Order Region Order State Product Category Id Product Name Product Price Shipping Mode target
48 PAYMENT -30.750000 115.180000 17 Bayamon Puerto Rico Home Office PR 4 Pacific Asia Mirzapur India 4.8 0.04 59.990002 -0.27 2 119.980003 115.180000 South Asia Uttar Pradesh 17 Perfect Fitness Perfect Rip Deck 59.990002 Second Class 0
49 PAYMENT -122.730003 79.180000 29 Caguas Puerto Rico Home Office PR 5 Pacific Asia Bursa Turquía 0.8 0.01 39.990002 -1.55 2 79.980003 79.180000 West Asia Bursa 29 Under Armour Girls' Toddler Spine Surge Runni 39.990002 Second Class 0
50 PAYMENT 33.599998 96.000000 24 Caguas Puerto Rico Home Office PR 5 Pacific Asia Murray Bridge Australia 4.0 0.04 50.000000 0.35 2 100.000000 96.000000 Oceania Australia del Sur 24 Nike Men's Dri-FIT Victory Golf Polo 50.000000 Second Class 0
51 PAYMENT 24.690001 75.980003 29 Caguas Puerto Rico Home Office PR 5 Pacific Asia Kartal Turquía 4.0 0.05 39.990002 0.33 2 79.980003 75.980003 West Asia Estambul 29 Under Armour Girls' Toddler Spine Surge Runni 39.990002 Second Class 0
52 PAYMENT 9.100000 91.000000 24 Caguas Puerto Rico Home Office PR 5 Pacific Asia Ulan Bator Mongolia 9.0 0.09 50.000000 0.10 2 100.000000 91.000000 Eastern Asia Ulán Bator 24 Nike Men's Dri-FIT Victory Golf Polo 50.000000 Second Class 0
In [195]:
# Porcentagem de fraude (1) e não fraude (0) em todo o dataset
df['target'].value_counts(1)
Out[195]:
target
0    0.977454
1    0.022546
Name: proportion, dtype: float64
In [196]:
fraude_ou_nao = df['target'].value_counts()

# Tamanho do gráfico
plt.figure(figsize=(8,6))

# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
    [0, 1], # valor no eixo x
    fraude_ou_nao.values, # valor no eixo y
    color = ['steelblue', 'lightcoral'] # cores das barras
)

# Define os rótulos do eixo x para 'Não Fraude' e 'Fraude'
plt.xticks([0, 1], ['Não Fraude', 'Fraude'])

# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de pedidos', fontsize = 12)

# Titulo, letra tamanho 14
plt.title('Quantidade de operações fraudulentas', fontsize = 16)

#plt.xticks(fraude_ou_nao.index.astype(str), fraude_ou_nao.index.astype(str))

# Adicionando a contagem em cima das barras
for bar in barra:
    yval = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)

plt.grid(False)

plt.show()
No description has been provided for this image

Percebe-se que o conjunto de dados é extremamente desbalanceado, contendo 97.7% de operações não fraudulentas e apenas 2.3% de operações fraudulentas.

In [197]:
# class weight
weights = df.target.value_counts(1)[0]/df.target.value_counts(1)[1]
In [198]:
# Divisão em X e y
X = df.drop(columns=['target'], axis = 1)
y = df.target
In [199]:
# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state = 42, stratify=y)
In [200]:
# Porcentagem de fraude (1) e não fraude (0) no conjunto de treino
y_train.value_counts(1)
Out[200]:
target
0    0.977453
1    0.022547
Name: proportion, dtype: float64
In [201]:
# Porcentagem de fraude (1) e não fraude (0) no conjunto de teste
y_test.value_counts(1)
Out[201]:
target
0    0.977456
1    0.022544
Name: proportion, dtype: float64
In [202]:
# Instanciando modelo XGBoost
modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')

# Instanciando modelo LightGBM
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = 8, num_leaves = 2^8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, is_unbalance=True, verbose=-1)

# Instanciando modelo catboost
modelo_CatBoost = CatBoostClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, random_state = 0, scale_pos_weight = weights, verbose = 0)

# Instanciando o modelo Balanced Random Forest
modelo_BRandom_Forest = BalancedRandomForestClassifier(n_estimators = 1000, max_depth = 8, random_state = 0, verbose = 0)

# 1) Executar os modelos com a validação cruzada
# 2) fazer uma feature engineering, no próprio código da validação cruzada
# 3) Para o melhor modelo, fazer uma feature selection (RFE que usa feature importance), após o código da validação cruzada
# 4) Por fim, para este melhor modelo, fazer uma tunagem de hiperparâmetros

Construção de uma função de validação cruzada - Stratified K-Fold¶

In [203]:
# Função para aplicação da validação cruzada para obtenção das métricas dos modelos

def validacao_cruzada(X, y, modelo, k, threshold):

    # Inicializando a função StratifiedKFold
    folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)

    # Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
    # em cada fold

    precisoes = list()
    revocacoes=list()
    acuracias=list()
    Medida_F1=list()
    precision_recall_auc=list()
    rocs_auc=list()
    cm_total = np.zeros((2, 2))

    # Será aplicado o método "split" no objeto folds, que retornará uma lista 
    # com os índices das instâncias que pertencem ao conjunto de treino e 
    # outra com os índices das instâncias que pertencem ao conjunto de teste
    
    for k, (train_index, test_index) in enumerate(folds.split(X,y)):
        print("=-"*6 + f"Fold: {k+1}" + "-="*6)
        
        # Dividindo os dados em treino e teste para cada um dos folds
        X_train_intern, y_train_intern = X.iloc[train_index, :], y.iloc[train_index]
        X_test_intern, y_test_intern = X.iloc[test_index, :], y.iloc[test_index]
        
        # train_index e test_index: São os índices das instâncias do conjunto 
        # de treino e teste, respectivamente, selecionados em cada um dos folds
        
        ###########################################
        ############## Preprocessing ##############
        ###########################################
    
        # Instanciando o CatBoost Encoder        
        encoder = CatBoostEncoder()

        # Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
        cat_imputer = SimpleImputer(strategy='most_frequent')
        
        # Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
        num_imputer = SimpleImputer(strategy='median')

        # Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
        cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
        num_pipeline = Pipeline([('imputer', num_imputer)])

        # feature engineering

        # Identifica as variáveis categóricas e numéricas
        cat_cols = X_train_intern.select_dtypes(include=['object']).columns
        num_cols = X_train_intern.select_dtypes(exclude=['object']).columns

        # Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
        X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
        X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])

        # Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
        X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
        X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])

        # Treinando o modelo
        modelo.fit(X_train_intern, y_train_intern)

        # Obtendo as probabilidades de cada registro pertencer a classe 1
        y_pred_proba = modelo.predict_proba(X_test_intern)[:, 1]

        # Obtendo as previsões do modelo
        y_pred = np.where(y_pred_proba > threshold, 1, 0)

        # Calculando a precisão e revocação para determinar a precision_recall_auc
        precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)

        # Calculando a matriz de confusão do fold
        cm_total += confusion_matrix(y_test_intern, y_pred)

        # Determinando as métricas para cada fold
        precisao_revocacao_auc = auc(revocacao, precisao)
        roc_auc = roc_auc_score(y_test_intern, y_pred)
        acuracia_score = accuracy_score(y_test_intern, y_pred)
        precisao_score = precision_score(y_test_intern, y_pred)
        revocacao_score = recall_score(y_test_intern, y_pred)
        f1score = f1_score(y_test_intern, y_pred)

        # Armazenando as métricas nas listas criadas
        precisoes.append(precisao_score)
        revocacoes.append(revocacao_score)
        precision_recall_auc.append(precisao_revocacao_auc)
        rocs_auc.append(roc_auc)
        acuracias.append(acuracia_score)
        Medida_F1.append(f1score)

        # Exibindo as métricas para cada um dos folds
        print(f"Precisão: {precisao_score:.4f}")
        print(f"Revocação: {revocacao_score:.4f}")
        print(f"Acurácia: {acuracia_score:.4f}")
        print(f"Medida F1: {f1score:.4f}")
        print(f"Precision-Recall AUC: {precisao_revocacao_auc:.4f}")
        print(f"ROC AUC: {roc_auc:.4f}")

    # Transformando as listas em arrays para fazer operações matemáticas
    precisoes = np.array(precisoes)
    revocacoes = np.array(revocacoes)
    precision_recall_auc = np.array(precision_recall_auc)
    rocs_auc = np.array(rocs_auc)
    acuracias = np.array(acuracias)
    Medida_F1 = np.array(Medida_F1)

    # Calculando as médias das métricas
    media_revocacao = np.mean(revocacoes)
    media_precisao = np.mean(precisoes)
    media_acuracia = np.mean(acuracias)
    media_F1 = np.mean(Medida_F1)
    media_pr_AUC = np.mean(precision_recall_auc)
    media_roc_AUC = np.mean(rocs_auc)

    # Calculando os desvios padrão para cada métrica
    std_revocacao = np.std(revocacoes)
    std_precisao = np.std(precisoes)
    std_acuracia = np.std(acuracias)
    std_F1 = np.std(Medida_F1)
    std_pr_AUC = np.std(precision_recall_auc)
    std_roc_AUC = np.std(rocs_auc)

    # Exibindo as médias das métricas obtidas
    print()
    print("=-"*6 + "Exibindo a média das métricas obtidas" + "-="*6)
    print(f"Média da acurácia: {media_acuracia:.4f} +/- {std_acuracia:.4f}")
    print(f"Média da revocação: {media_revocacao:.4f} +/- {std_revocacao:.4f}")
    print(f"Média da precisão: {media_precisao:.4f} +/- {std_precisao:.4f}")
    print(f"Média da Medida F1: {media_F1:.4f} +/- {std_F1:.4f}")
    print(f"Média da ROC AUC: {media_roc_AUC:.4f} +/- {std_roc_AUC:.4f}")
    print(f"Média da PR AUC: {media_pr_AUC:.4f} +/- {std_pr_AUC:.4f}")

    # Plotando a matriz de confusão agregada com heatmap
    plt.figure(figsize=(8, 6))
    sns.heatmap(cm_total, annot=True, fmt=".0f", cmap="Blues")
    plt.title("Matriz de Confusão Agregada de Todos os Folds")
    plt.ylabel('Verdadeiro')
    plt.xlabel('Previsto')
    plt.show()

Modelo LightGBM¶

In [204]:
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1483
Revocação: 0.9149
Acurácia: 0.8795
Medida F1: 0.2552
Precision-Recall AUC: 0.5326
ROC AUC: 0.8968
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1564
Revocação: 0.9085
Acurácia: 0.8874
Medida F1: 0.2668
Precision-Recall AUC: 0.5335
ROC AUC: 0.8977
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1555
Revocação: 0.9123
Acurácia: 0.8863
Medida F1: 0.2656
Precision-Recall AUC: 0.5348
ROC AUC: 0.8990
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1464
Revocação: 0.9265
Acurácia: 0.8766
Medida F1: 0.2528
Precision-Recall AUC: 0.5372
ROC AUC: 0.9009
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1532
Revocação: 0.9058
Acurácia: 0.8850
Medida F1: 0.2620
Precision-Recall AUC: 0.5306
ROC AUC: 0.8952

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.8830 +/- 0.0042
Média da revocação: 0.9136 +/- 0.0071
Média da precisão: 0.1519 +/- 0.0040
Média da Medida F1: 0.2605 +/- 0.0056
Média da ROC AUC: 0.8979 +/- 0.0020
Média da PR AUC: 0.5337 +/- 0.0022
No description has been provided for this image

Modelo XGBoost¶

In [205]:
validacao_cruzada(X, y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1417
Revocação: 0.9343
Acurácia: 0.8708
Medida F1: 0.2461
Precision-Recall AUC: 0.5387
ROC AUC: 0.9018
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1067
Revocação: 0.9729
Acurácia: 0.8155
Medida F1: 0.1923
Precision-Recall AUC: 0.5401
ROC AUC: 0.8924
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1018
Revocação: 0.9652
Acurácia: 0.8072
Medida F1: 0.1841
Precision-Recall AUC: 0.5339
ROC AUC: 0.8844
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1093
Revocação: 0.9665
Acurácia: 0.8217
Medida F1: 0.1963
Precision-Recall AUC: 0.5382
ROC AUC: 0.8924
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1181
Revocação: 0.9548
Acurácia: 0.8384
Medida F1: 0.2103
Precision-Recall AUC: 0.5370
ROC AUC: 0.8953

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.8307 +/- 0.0225
Média da revocação: 0.9587 +/- 0.0135
Média da precisão: 0.1155 +/- 0.0141
Média da Medida F1: 0.2058 +/- 0.0219
Média da ROC AUC: 0.8933 +/- 0.0056
Média da PR AUC: 0.5376 +/- 0.0021
No description has been provided for this image

Modelo CatBoost¶

In [206]:
validacao_cruzada(X, y, modelo_CatBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.0841
Revocação: 1.0000
Acurácia: 0.7542
Medida F1: 0.1551
Precision-Recall AUC: 0.5420
ROC AUC: 0.8742
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.0835
Revocação: 0.9974
Acurácia: 0.7528
Medida F1: 0.1540
Precision-Recall AUC: 0.5405
ROC AUC: 0.8723
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.0833
Revocação: 1.0000
Acurácia: 0.7520
Medida F1: 0.1538
Precision-Recall AUC: 0.5417
ROC AUC: 0.8732
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.0827
Revocação: 0.9987
Acurácia: 0.7505
Medida F1: 0.1528
Precision-Recall AUC: 0.5407
ROC AUC: 0.8717
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.0850
Revocação: 0.9948
Acurácia: 0.7585
Medida F1: 0.1566
Precision-Recall AUC: 0.5400
ROC AUC: 0.8740

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7536 +/- 0.0027
Média da revocação: 0.9982 +/- 0.0019
Média da precisão: 0.0837 +/- 0.0008
Média da Medida F1: 0.1545 +/- 0.0013
Média da ROC AUC: 0.8731 +/- 0.0010
Média da PR AUC: 0.5410 +/- 0.0008
No description has been provided for this image

Modelo Balanced Random Forest¶

In [207]:
validacao_cruzada(X, y, modelo_BRandom_Forest, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.0885
Revocação: 0.9910
Acurácia: 0.7694
Medida F1: 0.1624
Precision-Recall AUC: 0.5398
ROC AUC: 0.8776
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.0840
Revocação: 0.9961
Acurácia: 0.7547
Medida F1: 0.1549
Precision-Recall AUC: 0.5401
ROC AUC: 0.8726
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.0843
Revocação: 0.9987
Acurácia: 0.7554
Medida F1: 0.1554
Precision-Recall AUC: 0.5415
ROC AUC: 0.8742
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.0829
Revocação: 0.9987
Acurácia: 0.7511
Medida F1: 0.1531
Precision-Recall AUC: 0.5408
ROC AUC: 0.8721
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.0872
Revocação: 0.9897
Acurácia: 0.7662
Medida F1: 0.1602
Precision-Recall AUC: 0.5385
ROC AUC: 0.8754

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.7594 +/- 0.0071
Média da revocação: 0.9948 +/- 0.0038
Média da precisão: 0.0854 +/- 0.0021
Média da Medida F1: 0.1572 +/- 0.0035
Média da ROC AUC: 0.8744 +/- 0.0020
Média da PR AUC: 0.5402 +/- 0.0010
No description has been provided for this image

Entre os modelos escolhidos para avaliar a métrica revocação, o XGBoost foi o que melhor performou, com:

  • uma revocação média de 0.9587, ou seja, de todos os casos que realmente eram fraude, ele identificou 95,87%.
  • uma ROC AUC média de 0.8933, ou seja, há 89,33% de que o modelo classifique corretamente um pedido aleatório de fraude como fraude e um pedido aleatório de não fraude como não fraude.
  • uma Medida F1 média de 0.2058, o que indica que o modelo ainda possui muitos falsos positivos (precisão baixa).

Feature Selection¶

In [208]:
# Inicializando o RFE
rfe = RFE(estimator = modelo_XGBoost, n_features_to_select = 21, step = 1)

encoder = CatBoostEncoder()
# Ajustar e transformar os dados de treinamento
X_train_encoded = encoder.fit_transform(X_train, y_train)

# Transformar os dados de teste
X_test_encoded = encoder.transform(X_test)

# Treinando o RFE
rfe.fit(X_train_encoded, y_train)

# Obtendo as features selecionadas
features_selecionadas = np.array(list(X_train_encoded.columns))[rfe.support_]

features_selecionadas
Out[208]:
array(['Type', 'Benefit per order', 'Sales per customer', 'Category Id',
       'Customer City', 'Customer Country', 'Customer Segment',
       'Customer State', 'Market', 'Order City', 'Order Country',
       'Order Item Discount', 'Order Item Discount Rate',
       'Order Item Product Price', 'Order Item Profit Ratio',
       'Order Item Quantity', 'Sales', 'Order Region', 'Order State',
       'Product Name', 'Shipping Mode'], dtype='<U24')
In [209]:
validacao_cruzada(X[features_selecionadas], y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1417
Revocação: 0.9343
Acurácia: 0.8708
Medida F1: 0.2461
Precision-Recall AUC: 0.5387
ROC AUC: 0.9018
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1067
Revocação: 0.9729
Acurácia: 0.8155
Medida F1: 0.1923
Precision-Recall AUC: 0.5401
ROC AUC: 0.8924
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1018
Revocação: 0.9652
Acurácia: 0.8072
Medida F1: 0.1841
Precision-Recall AUC: 0.5339
ROC AUC: 0.8844
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1093
Revocação: 0.9665
Acurácia: 0.8217
Medida F1: 0.1963
Precision-Recall AUC: 0.5382
ROC AUC: 0.8924
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1181
Revocação: 0.9548
Acurácia: 0.8384
Medida F1: 0.2103
Precision-Recall AUC: 0.5370
ROC AUC: 0.8953

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.8307 +/- 0.0225
Média da revocação: 0.9587 +/- 0.0135
Média da precisão: 0.1155 +/- 0.0141
Média da Medida F1: 0.2058 +/- 0.0219
Média da ROC AUC: 0.8933 +/- 0.0056
Média da PR AUC: 0.5376 +/- 0.0021
No description has been provided for this image

Neste caso, realizando uma feature selection e excluindo as 3 colunas menos importantes para o modelos, atingimos o mesmo resultado que com o dataset original. Assim, por questões de processamento, é preferível manter o dataset com as 3 colunas a menos.

Tunagem de Hiperparâmetros para o XGBoost¶

In [210]:
def tunagem_hiperparametros(trial, k = 5, threshold = 0.5):

    # Parâmetros para serem tunados
    learning_rate = trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True)
    max_depth = trial.suggest_int('max_depth', 1, 20)
    subsample = trial.suggest_float('subsample', 0.5, 1, step = 0.1)
    colsample_bytree = trial.suggest_float('colsample_bytree', 0.5, 1, step = 0.1)
    min_child_weight = trial.suggest_int('min_child_weight', 1, 20)
    
    # Inicializando a função StratifiedKFold
    folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)

    # Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
    # em cada fold
    precisoes = list()
    revocacoes=list()
    acuracias=list()
    Medida_F1=list()
    precision_recall_auc=list()
    rocs_auc=list()

    # Será aplicado o método "split" no objeto folds, que retornará uma lista 
    # com os índices das instâncias que pertencem ao conjunto de treino e 
    # outra com os índices das instâncias que pertencem ao conjunto de teste

    for k, (train_index, test_index) in enumerate(folds.split(X[features_selecionadas],y)):
        print("=-"*6 + f"Fold: {k+1}" + "-="*6)

        # Dividindo os dados em treino e teste para cada um dos folds
        X_train_intern, y_train_intern = X[features_selecionadas].iloc[train_index, :], y.iloc[train_index]
        X_test_intern, y_test_intern = X[features_selecionadas].iloc[test_index, :], y.iloc[test_index]

        # train_index e test_index: São os índices das instâncias do conjunto 
        # de treino e teste, respectivamente, selecionados em cada um dos folds
        
        ###########################################
        ############## Preprocessing ##############
        ###########################################
    
        # Instanciando o CatBoost Encoder        
        encoder = CatBoostEncoder()

        # Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
        cat_imputer = SimpleImputer(strategy='most_frequent')
        
        # Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
        num_imputer = SimpleImputer(strategy='median')

        # Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
        cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
        num_pipeline = Pipeline([('imputer', num_imputer)])

        # Identifica as variáveis categóricas e numéricas
        cat_cols = X_train_intern.select_dtypes(include=['object']).columns
        num_cols = X_train_intern.select_dtypes(exclude=['object']).columns

        # Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
        X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
        X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])

        # Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
        X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
        X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])

        # Instanciando o Modelo XGBoost
        modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = max_depth, learning_rate = learning_rate, 
                                       subsample = subsample, colsample_bytree = colsample_bytree, min_child_weight = min_child_weight, 
                                       n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')

        # Treinando o modelo XGBoost
        modelo_XGBoost.fit(X_train_intern, y_train_intern)

        # Obtendo as probabilidades de cada registro pertencer a classe 1
        y_pred_proba = modelo_XGBoost.predict_proba(X_test_intern)[:, 1]

        # Obtendo as previsões do modelo
        y_pred = np.where(y_pred_proba > threshold, 1, 0)

        # Calculando a precisão e revocação para determinar a precision_recall_auc
        precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)

        # Determinando as métricas para cada fold
        precisao_revocacao_auc = auc(revocacao, precisao)
        roc_auc = roc_auc_score(y_test_intern, y_pred)
        acuracia_score = accuracy_score(y_test_intern, y_pred)
        precisao_score = precision_score(y_test_intern, y_pred)
        revocacao_score = recall_score(y_test_intern, y_pred)
        f1score = f1_score(y_test_intern, y_pred)

        # Armazenando as métricas nas listas criadas
        precisoes.append(precisao_score)
        revocacoes.append(revocacao_score)
        precision_recall_auc.append(precisao_revocacao_auc)
        rocs_auc.append(roc_auc)
        acuracias.append(acuracia_score)
        Medida_F1.append(f1score)

    # Transformando as listas em arrays para fazer operações matemáticas
    precisoes = np.array(precisoes)
    revocacoes = np.array(revocacoes)
    precision_recall_auc = np.array(precision_recall_auc)
    rocs_auc = np.array(rocs_auc)
    acuracias = np.array(acuracias)
    Medida_F1 = np.array(Medida_F1)

    # Calculando as médias das métricas
    media_revocacao = np.mean(revocacoes)
    media_precisao = np.mean(precisoes)
    media_acuracia = np.mean(acuracias)
    media_F1 = np.mean(Medida_F1)
    media_pr_AUC = np.mean(precision_recall_auc)
    media_roc_AUC = np.mean(rocs_auc)

    # Calculando os desvios padrão para cada métrica
    std_revocacao = np.std(revocacoes)
    std_precisao = np.std(precisoes)
    std_acuracia = np.std(acuracias)
    std_F1 = np.std(Medida_F1)
    std_pr_AUC = np.std(precision_recall_auc)
    std_roc_AUC = np.std(rocs_auc)
    
    return media_roc_AUC


study = opt.create_study(direction='maximize')
study.optimize(tunagem_hiperparametros, n_trials = 20)
[I 2024-02-10 22:50:27,117] A new study created in memory with name: no-name-75910072-fd13-43c0-9c8c-4c56e7363ec8
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:51:34,699] Trial 0 finished with value: 0.8795904611780354 and parameters: {'learning_rate': 0.07994256492342769, 'max_depth': 1, 'subsample': 0.5, 'colsample_bytree': 0.8, 'min_child_weight': 3}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:53:38,275] Trial 1 finished with value: 0.8195058699436866 and parameters: {'learning_rate': 0.03857576834279582, 'max_depth': 7, 'subsample': 0.9, 'colsample_bytree': 0.5, 'min_child_weight': 6}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:54:54,145] Trial 2 finished with value: 0.8748614167956793 and parameters: {'learning_rate': 0.010246492195636328, 'max_depth': 2, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_weight': 15}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:58:49,145] Trial 3 finished with value: 0.8842436038946081 and parameters: {'learning_rate': 0.001155522474633838, 'max_depth': 12, 'subsample': 1.0, 'colsample_bytree': 0.6, 'min_child_weight': 3}. Best is trial 3 with value: 0.8842436038946081.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:00:23,475] Trial 4 finished with value: 0.8874130681818576 and parameters: {'learning_rate': 0.011912842014604687, 'max_depth': 3, 'subsample': 0.6, 'colsample_bytree': 0.7, 'min_child_weight': 12}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:02:31,563] Trial 5 finished with value: 0.7679785058549813 and parameters: {'learning_rate': 0.09516642910100707, 'max_depth': 6, 'subsample': 0.8, 'colsample_bytree': 1.0, 'min_child_weight': 19}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:05:51,242] Trial 6 finished with value: 0.7251617956877033 and parameters: {'learning_rate': 0.032019024764589774, 'max_depth': 11, 'subsample': 0.6, 'colsample_bytree': 1.0, 'min_child_weight': 14}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:11:36,789] Trial 7 finished with value: 0.740677298749992 and parameters: {'learning_rate': 0.009486734213922966, 'max_depth': 19, 'subsample': 0.6, 'colsample_bytree': 1.0, 'min_child_weight': 9}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:15:25,768] Trial 8 finished with value: 0.6253362874109603 and parameters: {'learning_rate': 0.07718828159729633, 'max_depth': 16, 'subsample': 0.9, 'colsample_bytree': 0.6, 'min_child_weight': 8}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:16:55,594] Trial 9 finished with value: 0.8977030594763274 and parameters: {'learning_rate': 0.06716806389913002, 'max_depth': 3, 'subsample': 0.9, 'colsample_bytree': 1.0, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:18:43,054] Trial 10 finished with value: 0.8976164627035237 and parameters: {'learning_rate': 0.002689437912090455, 'max_depth': 7, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:20:33,669] Trial 11 finished with value: 0.8952878930728498 and parameters: {'learning_rate': 0.001699179220927879, 'max_depth': 7, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:22:16,183] Trial 12 finished with value: 0.8881887720731031 and parameters: {'learning_rate': 0.0027558417106266398, 'max_depth': 5, 'subsample': 0.9, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:25:00,732] Trial 13 finished with value: 0.9011311412130854 and parameters: {'learning_rate': 0.004259837949378204, 'max_depth': 9, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_weight': 5}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:29:34,596] Trial 14 finished with value: 0.7954917222923114 and parameters: {'learning_rate': 0.005175092414261333, 'max_depth': 14, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_weight': 6}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:32:26,844] Trial 15 finished with value: 0.7971755825236846 and parameters: {'learning_rate': 0.020161930390439852, 'max_depth': 9, 'subsample': 0.7, 'colsample_bytree': 1.0, 'min_child_weight': 5}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:34:56,655] Trial 16 finished with value: 0.8995262369826722 and parameters: {'learning_rate': 0.005083639900788686, 'max_depth': 9, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_weight': 4}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:37:38,630] Trial 17 finished with value: 0.891385851763976 and parameters: {'learning_rate': 0.00518511506281332, 'max_depth': 10, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_weight': 9}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-11 11:08:17,560] Trial 18 finished with value: 0.7776167122971407 and parameters: {'learning_rate': 0.004786985768187549, 'max_depth': 14, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_weight': 4}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-11 11:11:21,440] Trial 19 finished with value: 0.8504330954716037 and parameters: {'learning_rate': 0.003381270572925951, 'max_depth': 13, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_weight': 7}. Best is trial 13 with value: 0.9011311412130854.
In [211]:
params = {'learning_rate': 0.006273985142858805, 'max_depth': 7, 'subsample': 0.7, 'colsample_bytree': 0.8, 'min_child_weight': 12}
In [212]:
# XGBoost executado para os melhores parâmetros
modelo_XGBoost = XGBClassifier(n_estimators = 1000, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error', **params)
In [213]:
validacao_cruzada(X[features_selecionadas], y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1431
Revocação: 0.9369
Acurácia: 0.8720
Medida F1: 0.2482
Precision-Recall AUC: 0.5407
ROC AUC: 0.9037
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1261
Revocação: 0.9665
Acurácia: 0.8481
Medida F1: 0.2230
Precision-Recall AUC: 0.5467
ROC AUC: 0.9059
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1359
Revocação: 0.9484
Acurácia: 0.8629
Medida F1: 0.2377
Precision-Recall AUC: 0.5427
ROC AUC: 0.9047
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1282
Revocação: 0.9535
Acurácia: 0.8528
Medida F1: 0.2260
Precision-Recall AUC: 0.5414
ROC AUC: 0.9020
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1356
Revocação: 0.9484
Acurácia: 0.8626
Medida F1: 0.2373
Precision-Recall AUC: 0.5426
ROC AUC: 0.9045

=-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-=
Média da acurácia: 0.8597 +/- 0.0084
Média da revocação: 0.9507 +/- 0.0096
Média da precisão: 0.1338 +/- 0.0061
Média da Medida F1: 0.2344 +/- 0.0091
Média da ROC AUC: 0.9042 +/- 0.0013
Média da PR AUC: 0.5428 +/- 0.0021
No description has been provided for this image

Com a realização da tunagem de hiperparâmetros:

  • A ROC AUC saltou de 0.8933 para 0.9042
  • A revocação praticamente se manteve a mesma
  • A precisão saltou de 0.1155 para 0.1338
  • A Medida F1 saltou de 0.2058 para 0.2344
  • A PR AUC saltou de 0.5376 para 0.5428
  • A acurácia saltou de 0.8307 para 0.8597

No geral houve uma pequena melhora no modelo.

O quanto a DataCo Global ganhou ao identificar corretamente as fraudes?¶

In [214]:
encoder = CatBoostEncoder()

X_train_encoded = encoder.fit_transform(X_train[features_selecionadas], y_train)
X_test_encoded = encoder.transform(X_test[features_selecionadas])

# Fazendo previsões de probabilidade de fraude para o conjunto de teste
xgb_probs = modelo_XGBoost.predict_proba(X_test_encoded)[:, 1]

# Aqui são trazidas as probabilidades encontradas pelo XGBoost

df_test = X_test[features_selecionadas].copy()
df_test['fraude'] = y_test
df_test['XGB_Prob'] = xgb_probs
In [215]:
# Função para calcular o impacto financeiro das decisões de bloqueio de transações

def calculo_impacto_financeiro(df, blocked_col, fraud_col, profit_col):
    
    # Calculando perdas por fraude (transações que são fraudes e não foram bloqueadas)
    df['fraud_loss'] = ((df[fraud_col]) & (~df[blocked_col])) * df[profit_col]
    
    # lucro obtido de transações legítimas que o sistema corretamente identificou como não fraudulentas e, portanto, não bloqueou.
    df['saved_profit'] = ((~df[fraud_col]) & (~df[blocked_col])) * df[profit_col]
    
    # Representa o lucro líquido após considerar tanto as perdas por fraude quanto o lucro preservado.
    df['total_profit'] = df['saved_profit'] - df['fraud_loss']
    
    return df[['fraud_loss', 'saved_profit', 'total_profit']].sum()
In [216]:
# Definindo uma gama de limiares possíveis
possiveis_thresholds = np.linspace(0.01, 0.99, 99)

# Inicializando uma lista para armazenar os resultados
impactos_financeiros = []

# Testando cada limiar
for threshold in possiveis_thresholds:
    # Aplicando o limiar atual
    df_test['blocked'] = df_test['XGB_Prob'] >= threshold
    
    # Calculando o impacto financeiro para o limiar atual
    impacto = calculo_impacto_financeiro(df_test, 'blocked', 'fraude', 'Benefit per order')
    
    # Armazenando os resultados, incluindo o limiar
    impactos_financeiros.append({
        'threshold': threshold, 
        'Perda por fraude': impacto['fraud_loss'],
        'Lucro Salvo': impacto['saved_profit'],
        'Lucro Total': impacto['total_profit']
    })

# Convertendo os resultados em um DataFrame
results_df = pd.DataFrame(impactos_financeiros)

# Encontrando o limiar com o maior lucro
best_result = results_df.loc[results_df['Lucro Total'].idxmax()]
best_result_df = pd.DataFrame([best_result])

# Exibindo o melhor limiar e o lucro associado
best_result_df
Out[216]:
threshold Perda por fraude Lucro Salvo Lucro Total
96 0.97 24241.450006 1.090407e+06 1.066166e+06
In [217]:
print(f"O modelo conseguiu lucrar {best_result_df['Lucro Salvo'].iloc[0]:.2f} ao identificar corretamente operações fraudulentas!")
print(f"Porém, modelo perdeu {best_result_df['Perda por fraude'].iloc[0]:.2f} por não identificar corretamente outras operações que realmente eram fraudes!")
print(f"Considerando as perdas por fraude, o modelo conseguiu lucrar {best_result_df['Lucro Total'].iloc[0]:.2f} ao total!")
O modelo conseguiu lucrar 1090407.07 ao identificar corretamente operações fraudulentas!
Porém, modelo perdeu 24241.45 por não identificar corretamente outras operações que realmente eram fraudes!
Considerando as perdas por fraude, o modelo conseguiu lucrar 1066165.62 ao total!
In [ ]: